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

@@ -5,6 +5,12 @@ set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(MEAN_FIELD_UNIFORM_POLYNOMIAL_ORDER_INCREMENT 0 CACHE STRING
"Uniform increment applied to every registered finite-element family order")
if (NOT MEAN_FIELD_UNIFORM_POLYNOMIAL_ORDER_INCREMENT MATCHES "^[0-9]+$")
message(FATAL_ERROR "MEAN_FIELD_UNIFORM_POLYNOMIAL_ORDER_INCREMENT must be a non-negative integer")
endif ()
add_compile_options(
-gdwarf-4
-Wno-unused-parameter
@@ -34,6 +40,11 @@ pkg_check_modules(stroid REQUIRED IMPORTED_TARGET stroid)
add_library(mean_field)
target_compile_definitions(mean_field
PUBLIC
MEAN_FIELD_UNIFORM_POLYNOMIAL_ORDER_INCREMENT=${MEAN_FIELD_UNIFORM_POLYNOMIAL_ORDER_INCREMENT}
)
target_include_directories(mean_field
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/libmeanfield/include>
@@ -57,6 +68,8 @@ target_sources(mean_field
libmeanfield/impl/integrators/viscosity.cpp
libmeanfield/impl/mapping/domain_mapper.cpp
libmeanfield/impl/mapping/transformations.cpp
libmeanfield/impl/deformation/nodal_radial_surface.cpp
libmeanfield/impl/deformation/radial_extensions.cpp
libmeanfield/impl/operators/gravity_field.cpp
libmeanfield/impl/operators/gravity_field_jacobian.cpp
libmeanfield/impl/operators/kernels/gravity_kernels.cpp
@@ -152,6 +165,13 @@ target_sources(mean_field
libmeanfield/interface/surface/dependencies.cppm
libmeanfield/interface/surface/compiled.cppm
libmeanfield/interface/surface/compiler.cppm
libmeanfield/interface/deformation/descriptors.cppm
libmeanfield/interface/deformation/surface_prescription.cppm
libmeanfield/interface/deformation/nodal_radial_surface.cppm
libmeanfield/interface/deformation/interior_extension.cppm
libmeanfield/interface/deformation/vacuum_extension.cppm
libmeanfield/interface/deformation/radial_extensions.cppm
libmeanfield/interface/deformation/domain_deformation.cppm
libmeanfield/interface/models/stellar_model.cppm
libmeanfield/interface/operators/prepared_mass_normalization.cppm
libmeanfield/interface/operators/prepared_centering_constraint.cppm
@@ -231,6 +251,11 @@ add_executable(tests
tests/operators/prepared_rotation_displacement_force_affine_deformation.cpp
tests/operators/prepared_displacement_operator.cpp
tests/models/stellar_model.cpp
tests/deformation/contracts.cpp
tests/deformation/surface_scalar_dof_map.cpp
tests/deformation/nodal_radial_surface.cpp
tests/deformation/radial_extensions.cpp
tests/deformation/domain_deformation.cpp
tests/operators/prepared_mass_normalization.cpp
tests/operators/prepared_stellar_equilibrium.cpp
tests/utils/domain.cpp
@@ -266,6 +291,7 @@ add_executable(stellar_null_space_experiments
experiments/experiment_main.cpp
experiments/rigid_motion_null_space.cpp
experiments/gravity_completed_rigid_motion.cpp
experiments/coupled_gauge_modes.cpp
)
target_link_libraries(stellar_null_space_experiments

View File

@@ -24,43 +24,119 @@ Run only the budget and choose its output path with:
./mean_field_experiments --experiment-output gravity_budget.csv --catch2 "[accuracy]"
```
## Stellar-equilibrium null-space experiments
## Reduced stellar-surface conditioning experiments
`stellar_null_space_experiments` is a dedicated diagnostic executable rather
than an ordinary verification or validation test. It constructs the analytic
`n = 3` Lane-Emden seed, probes the three computational translations and three
computational rotations, and compares the Jacobian before and after the strong
centering-row replacement. It records total and residual-block response norms,
the isolated centering contribution, and centered finite-difference errors.
`n = 3` Lane-Emden seed and probes only directions representable by the reduced
surface coordinates: uniform radial homology, three translation-like radial
dipoles, an axisymmetric oblate quadrupole, and a degree-12 zonal spherical
harmonic. Tangential, rotational, stellar-interior-only, and vacuum-only mesh
motions are deliberately absent because they are generated coordinates rather
than root unknowns.
The experiment prints rank-zero progress messages while it builds the seed,
solves its gravity field, and completes each rigid-mode case. Run it with:
solves its gravity field, and completes each surface-mode case. The reachability
probe records the surface-to-volume lift amplification, complete root Jacobian
response by block, and centered-difference agreement at zero and half the
Keplerian angular speed. Run it with:
```text
mpirun -np 1 ./cmake-build-debug-homebrew/stellar_null_space_experiments \
--experiment-output stellar_null_space.csv \
--catch2 "[null_space][rigid_motion]"
--experiment-output reduced_surface_reachability.csv \
--catch2 "[null_space][surface_modes][reachability]"
```
The rotation sweep includes zero rotation and a spherical-state diagnostic at
half the Keplerian angular speed. The rotating result is an operator-symmetry
probe, not a definitive rotating-equilibrium null-space measurement.
The gravity-completed probe solves the linearized mixed gravity subsystem for
the gravity-gradient and gravity-potential variations accompanying each rigid
displacement. It then measures the complete equilibrium response with and
without the centering rows:
the gravity-gradient and gravity-potential variations accompanying each
reduced surface mode. It then measures the complete reduced root response:
```text
mpirun -np 1 ./cmake-build-debug-homebrew/stellar_null_space_experiments \
--experiment-output gravity_completed_null_space.csv \
--catch2 "[null_space][gravity_completed]"
--experiment-output gravity_completed_surface_modes.csv \
--catch2 "[null_space][surface_modes][gravity_completed]"
```
The gravity solver prints its convergence summary, while the experiment prints
the current mode and completed-case count. This probe prepares each rotation
state only once and does not repeat the expensive nonlinear finite-difference
calculations from the original rigid-motion diagnostic.
calculations from the reachability diagnostic.
The surface-frequency probe injects normalized zonal spherical harmonics over
a range of angular degrees. For each degree it lifts the unit surface pattern
once, samples the coefficients of
`det(I + a grad(d))` at the production geometry-inspection points, and locates
the positive and negative critical fractional amplitudes without repeatedly
rebuilding trial geometries. It also records the minimum determinant at
fractional amplitudes `1e-4`, `1e-3`, and `1e-2`:
```text
mpirun -np 1 ./cmake-build-debug-homebrew/stellar_null_space_experiments \
--experiment-output surface_frequency_limits.csv \
--catch2 "[surface_modes][frequency_limit]"
```
The coupled conditioning probe evaluates an extension-aware `n = 3` homology
direction together with the nonuniform reduced surface modes. Its density and
enthalpy tangents include the coordinate-composition terms generated by the
non-affine interior extension, and its gravity variation is completed through
the discrete mixed subsystem so the fixed-infinity exterior response is
consistent. The probe prepares the equilibrium once, reuses one restricted
gravity operator and preconditioner, and uses analytic Jacobian actions:
```text
mpirun -np 1 ./cmake-build-release-homebrew/stellar_null_space_experiments \
--experiment-output coupled_surface_conditioning.csv \
--catch2 "[null_space][surface_modes][conditioning]"
```
The CSV reports prescribed and gravity-completed responses by residual block,
the response normalized by the completed direction, lift conditioning where
applicable, and the convergence of each restricted gravity solve.
The homology mass-cancellation experiment evaluates the signed decomposition
```text
delta M = delta M_density + delta M_geometry
```
without solving gravity or preparing the complete coupled operator. The two
default-build cases provide the registered-order baseline and one uniform
spatial refinement:
```text
mpirun -np 1 ./cmake-build-release-homebrew/stellar_null_space_experiments \
--experiment-output homology_mass_h0_p0.csv \
--catch2 "[null_space][homology][mass_normalization][p_refinement]"
mpirun -np 1 ./cmake-build-release-homebrew/stellar_null_space_experiments \
--experiment-output homology_mass_h1_p0.csv \
--catch2 "[null_space][homology][mass_normalization][h_refinement]"
```
A reproducible one-level uniform polynomial refinement uses a separate build so
all registered field families and their quadrature policies see the same
compile-time order increment:
```text
cmake -S . -B cmake-build-release-homebrew-p1 -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_MAKE_PROGRAM=/opt/homebrew/bin/ninja \
-DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang \
-DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ \
-DUMFPACK_DIR=/opt/homebrew/lib/cmake/UMFPACK \
-DXAD_DIR=/usr/local/lib/cmake/XAD \
-Dhypre_DIR=/usr/local/lib/cmake/HYPRE \
-Dmfem_DIR=/usr/local/lib/cmake/mfem \
-DBoost_DIR=/opt/homebrew/anaconda3/lib/cmake/Boost-1.82.0 \
-DMEAN_FIELD_UNIFORM_POLYNOMIAL_ORDER_INCREMENT=1
cmake --build cmake-build-release-homebrew-p1 \
--target stellar_null_space_experiments -j 8
mpirun -np 1 \
./cmake-build-release-homebrew-p1/stellar_null_space_experiments \
--experiment-output homology_mass_h0_p1.csv \
--catch2 "[null_space][homology][mass_normalization][p_refinement]"
```
A whole-Jacobian dense singular-value experiment is intentionally deferred.
The checked-in `sandbox.smesh` is too large for a useful dense SVD, and the

View File

@@ -0,0 +1,694 @@
#include <catch2/catch_test_macros.hpp>
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <map>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include <mfem.hpp>
#include <mpi.h>
import experiment;
import experiment.stellar_null_space;
import mean_field;
import test_helpers;
namespace {
namespace null_space = experiment::null_space;
struct GaugeMode final {
std::string name;
std::string family;
int axis{-1};
bool requiresGravityCompletion{true};
mfem::Vector direction;
};
class GravityUnknownJacobian final : public mfem::Operator {
public:
explicit GravityUnknownJacobian(
const mean_field::operators::PreparedStellarEquilibriumOperator &stellarOperator
)
: mfem::Operator(
stellarOperator.GetLayout().size(null_space::gravityGradientValue) +
stellarOperator.GetLayout().size(null_space::gravityPotentialValue)
),
m_stellarOperator(stellarOperator),
m_gravityGradientSize(stellarOperator.GetLayout().size(null_space::gravityGradientValue)) {
MFEM_VERIFY(Width() == Height(), "The restricted gravity Jacobian must be square.");
}
void Mult(
const mfem::Vector &gravityDirection,
mfem::Vector &gravityAction
) const override {
MFEM_VERIFY(gravityDirection.Size() == Width(), "The restricted gravity direction has the wrong size.");
const mfem::Vector gravityGradientDirection(
const_cast<mfem::real_t *>(gravityDirection.GetData()), m_gravityGradientSize
);
const mfem::Vector gravityPotentialDirection(
const_cast<mfem::real_t *>(gravityDirection.GetData()) + m_gravityGradientSize,
Width() - m_gravityGradientSize
);
m_stellarOperator.GetGravityOperator().ApplyGravityUnknowns(
gravityGradientDirection, gravityPotentialDirection,
m_stellarOperator.GetGravityContext().GetGeometryContext(), gravityAction
);
}
[[nodiscard]] int gravity_gradient_size() const noexcept {
return m_gravityGradientSize;
}
private:
const mean_field::operators::PreparedStellarEquilibriumOperator &m_stellarOperator;
int m_gravityGradientSize;
};
struct GravityCompletionResult final {
mfem::Vector direction;
double rightHandSideNorm{0.0};
double residualNorm{0.0};
double relativeResidual{0.0};
double finalNorm{0.0};
int iterations{0};
bool solvePerformed{false};
};
void add_block_metrics(
std::map<
std::string,
double> &metrics,
const std::string &prefix,
const std::array<
double,
6> &norms
) {
for (std::size_t block = 0; block < norms.size(); ++block) {
metrics.emplace(prefix + null_space::residualBlockNames[block] + "_norm", norms[block]);
}
}
[[nodiscard]] mfem::Vector gravity_residual_blocks(
const mfem::Vector &completeAction,
const mean_field::operators::StellarEquilibriumLayout &layout
) {
const mfem::Vector gradient =
null_space::const_residual_view(completeAction, layout, null_space::gravityGradientResidual);
const mfem::Vector potential =
null_space::const_residual_view(completeAction, layout, null_space::gravityPotentialResidual);
mfem::Vector result(gradient.Size() + potential.Size());
mfem::Vector(result.GetData(), gradient.Size()) = gradient;
mfem::Vector(result.GetData() + gradient.Size(), potential.Size()) = potential;
return result;
}
void assign_gravity_completion(
mfem::Vector &completeDirection,
const mean_field::operators::StellarEquilibriumLayout &layout,
const mfem::Vector &gravityCompletion,
const int gravityGradientSize
) {
const mfem::Vector gravityGradient(
const_cast<mfem::real_t *>(gravityCompletion.GetData()), gravityGradientSize
);
const mfem::Vector gravityPotential(
const_cast<mfem::real_t *>(gravityCompletion.GetData()) + gravityGradientSize,
gravityCompletion.Size() - gravityGradientSize
);
null_space::assign_value_block(completeDirection, layout, null_space::gravityGradientValue, gravityGradient);
null_space::assign_value_block(completeDirection, layout, null_space::gravityPotentialValue, gravityPotential);
}
[[nodiscard]] GravityCompletionResult solve_gravity_completion(
const mfem::Vector &prescribedAction,
const mean_field::operators::StellarEquilibriumLayout &layout,
const MPI_Comm communicator,
GravityUnknownJacobian &gravityJacobian,
mfem::MINRESSolver &gravitySolver
) {
mfem::Vector rightHandSide = gravity_residual_blocks(prescribedAction, layout);
rightHandSide *= -1.0;
GravityCompletionResult result;
result.direction.SetSize(gravityJacobian.Width());
result.direction = 0.0;
result.rightHandSideNorm = null_space::global_norm(rightHandSide, communicator);
const double skipThreshold = 100.0 * std::numeric_limits<double>::epsilon();
if (result.rightHandSideNorm <= skipThreshold) {
return result;
}
gravitySolver.Mult(rightHandSide, result.direction);
REQUIRE(gravitySolver.GetConverged());
mfem::Vector action;
gravityJacobian.Mult(result.direction, action);
action -= rightHandSide;
result.residualNorm = null_space::global_norm(action, communicator);
result.relativeResidual = result.residualNorm / result.rightHandSideNorm;
result.finalNorm = gravitySolver.GetFinalNorm();
result.iterations = gravitySolver.GetNumIterations();
result.solvePerformed = true;
REQUIRE(std::isfinite(result.relativeResidual));
return result;
}
class ExtensionAwareHomologyScalarCoefficient final : public mfem::Coefficient {
public:
ExtensionAwareHomologyScalarCoefficient(
const mfem::ParGridFunction &baseField,
const mfem::ParGridFunction &coordinateVelocity,
const mfem::Vector &referenceCenter,
const double physicalScalingExponent
)
: m_baseField(&baseField),
m_coordinateVelocity(&coordinateVelocity),
m_referenceCenter(&referenceCenter),
m_physicalScalingExponent(physicalScalingExponent) {
}
double Eval(
mfem::ElementTransformation &transformation,
const mfem::IntegrationPoint &integrationPoint
) override {
transformation.SetIntPoint(&integrationPoint);
mfem::Vector referencePosition;
mfem::Vector coordinateVelocity;
mfem::Vector baseGradient;
transformation.Transform(integrationPoint, referencePosition);
m_coordinateVelocity->GetVectorValue(transformation, integrationPoint, coordinateVelocity);
m_baseField->GetGradient(transformation, baseGradient);
coordinateVelocity -= referencePosition;
coordinateVelocity += *m_referenceCenter;
return -m_physicalScalingExponent * m_baseField->GetValue(transformation, integrationPoint) +
baseGradient * coordinateVelocity;
}
private:
const mfem::ParGridFunction *m_baseField;
const mfem::ParGridFunction *m_coordinateVelocity;
const mfem::Vector *m_referenceCenter;
double m_physicalScalingExponent;
};
[[nodiscard]] mfem::Vector project_extension_aware_homology_scalar(
mfem::ParFiniteElementSpace &finiteElementSpace,
const mean_field::field::FieldDofMap &fieldMap,
const mfem::Vector &baseReducedField,
const mfem::ParGridFunction &coordinateVelocity,
const mfem::Vector &referenceCenter,
const double physicalScalingExponent
) {
mfem::ParGridFunction baseField(&finiteElementSpace);
baseField.SetFromTrueDofs(fieldMap.scatter(baseReducedField));
ExtensionAwareHomologyScalarCoefficient coefficient(
baseField, coordinateVelocity, referenceCenter, physicalScalingExponent
);
mfem::ParGridFunction directionField(&finiteElementSpace);
directionField.ProjectCoefficient(coefficient);
mfem::Vector directionTrue;
directionField.GetTrueDofs(directionTrue);
return fieldMap.gather(directionTrue);
}
struct HomologyMassCancellation final {
double currentMass{0.0};
double targetMass{0.0};
double densityContribution{0.0};
double geometryContribution{0.0};
double completeDerivative{0.0};
};
[[nodiscard]] HomologyMassCancellation measure_homology_mass_cancellation(
const mean_field::operators::PreparedMassNormalizationOperator &massOperator,
const mfem::Vector &densityDirection,
const mfem::Vector &volumeDirection
) {
mfem::Vector densityAction;
mfem::Vector geometryAction;
mfem::Vector completeAction;
massOperator.ApplyDensityJacobianAction(densityDirection, densityAction);
massOperator.ApplyDisplacementJacobianAction(volumeDirection, geometryAction);
massOperator.ApplyCompleteJacobianAction(densityDirection, volumeDirection, completeAction);
REQUIRE(densityAction.Size() == 1);
REQUIRE(geometryAction.Size() == 1);
REQUIRE(completeAction.Size() == 1);
const double recomposedDerivative = densityAction(0) + geometryAction(0);
const double comparisonScale = std::max({1.0, std::abs(recomposedDerivative), std::abs(completeAction(0))});
CHECK(
std::abs(completeAction(0) - recomposedDerivative) <=
64.0 * std::numeric_limits<double>::epsilon() * comparisonScale
);
return {
.currentMass = massOperator.GetCurrentMass(),
.targetMass = massOperator.GetTargetMass(),
.densityContribution = densityAction(0),
.geometryContribution = geometryAction(0),
.completeDerivative = completeAction(0)
};
}
void add_homology_mass_metrics(
std::map<
std::string,
double> &metrics,
const HomologyMassCancellation &cancellation
) {
const double uncancelledMagnitude =
std::abs(cancellation.densityContribution) + std::abs(cancellation.geometryContribution);
const double targetScale = std::max(std::abs(cancellation.targetMass), std::numeric_limits<double>::epsilon());
metrics.emplace("current_mass", cancellation.currentMass);
metrics.emplace("target_mass", cancellation.targetMass);
metrics.emplace("base_mass_residual", cancellation.currentMass - cancellation.targetMass);
metrics.emplace(
"relative_base_mass_residual", (cancellation.currentMass - cancellation.targetMass) / targetScale
);
metrics.emplace("density_mass_derivative", cancellation.densityContribution);
metrics.emplace("geometry_mass_derivative", cancellation.geometryContribution);
metrics.emplace("complete_mass_derivative", cancellation.completeDerivative);
metrics.emplace("mass_derivative_uncancelled_magnitude", uncancelledMagnitude);
metrics.emplace(
"mass_derivative_relative_cancellation_error",
std::abs(cancellation.completeDerivative) /
std::max(uncancelledMagnitude, std::numeric_limits<double>::epsilon())
);
metrics.emplace("complete_mass_derivative_per_target_mass", cancellation.completeDerivative / targetScale);
}
[[nodiscard]] GaugeMode make_homology_mode(
null_space::N3Equilibrium &fixture,
const null_space::SurfaceMode &uniformRadialMode
) {
const auto &layout = fixture.stellar_operator().GetLayout();
const auto &state = fixture.state();
mfem::Vector direction(layout.value_offsets().Last());
direction = 0.0;
const mfem::Vector volumeDirection = fixture.lifted_surface_direction(uniformRadialMode.direction);
mfem::ParGridFunction coordinateVelocity(fixture.fem().displacementFes.get());
coordinateVelocity.SetFromTrueDofs(volumeDirection);
const mean_field::field::FieldDofMap densityMap =
mean_field::field::make_field_dof_map<mean_field::field::Density, null_space::DomainSchema>(
*fixture.fem().densityFes
);
const mean_field::field::FieldDofMap enthalpyMap =
mean_field::field::make_field_dof_map<mean_field::field::Enthalpy, null_space::DomainSchema>(
*fixture.fem().enthalpyFes
);
const mfem::Vector &referenceCenter = fixture.model().surfaceDeformationPrescription().referenceCenter();
const mfem::Vector densityDirection = project_extension_aware_homology_scalar(
*fixture.fem().densityFes, densityMap,
null_space::const_value_view(state, layout, null_space::densityValue), coordinateVelocity, referenceCenter,
3.0
);
null_space::assign_value_block(direction, layout, null_space::densityValue, densityDirection);
null_space::assign_value_block(
direction, layout, null_space::surfaceDeformationValue,
null_space::const_value_view(uniformRadialMode.direction, layout, null_space::surfaceDeformationValue)
);
/*
* A physical homology scales rho and h, while the power-law mesh
* extension moves interior coordinates non-affinely. The scalar
* tangents therefore contain the coordinate-composition term
* grad(f) dot (v - (X-Xc)) in addition to their physical scaling.
* Gravity is completed through the discrete mixed subsystem below,
* which also supplies the correct fixed-infinity exterior response.
*/
const mfem::Vector enthalpyDirection = project_extension_aware_homology_scalar(
*fixture.fem().enthalpyFes, enthalpyMap,
null_space::const_value_view(state, layout, null_space::enthalpyValue), coordinateVelocity, referenceCenter,
1.0
);
null_space::assign_value_block(direction, layout, null_space::enthalpyValue, enthalpyDirection);
null_space::value_view(direction, layout, null_space::bernoulliValue)(0) =
-null_space::const_value_view(state, layout, null_space::bernoulliValue)(0);
return {
.name = "n3_homology",
.family = "homology",
.axis = -1,
.requiresGravityCompletion = true,
.direction = std::move(direction)
};
}
[[nodiscard]] std::vector<GaugeMode> make_gauge_modes(null_space::N3Equilibrium &fixture) {
auto surfaceModes = null_space::make_surface_modes(fixture);
const auto homologyMode = std::ranges::find_if(surfaceModes, [](const null_space::SurfaceMode &mode) {
return mode.kind == null_space::SurfaceModeKind::uniform_radial;
});
MFEM_VERIFY(homologyMode != surfaceModes.end(), "The reduced surface modes do not contain homology.");
std::vector<GaugeMode> modes;
modes.reserve(6);
modes.push_back(make_homology_mode(fixture, *homologyMode));
for (auto &surfaceMode : surfaceModes) {
if (surfaceMode.kind == null_space::SurfaceModeKind::uniform_radial) {
continue;
}
modes.push_back(
{.name = surfaceMode.name,
.family = null_space::surface_mode_kind_name(surfaceMode.kind),
.axis = surfaceMode.axis,
.requiresGravityCompletion = true,
.direction = std::move(surfaceMode.direction)}
);
}
return modes;
}
[[nodiscard]] long long global_nonzero_count(
const mfem::Vector &vector,
const MPI_Comm communicator
) {
long long localCount = 0;
for (int index = 0; index < vector.Size(); ++index) {
if (vector(index) != 0.0) {
++localCount;
}
}
long long globalCount = 0;
MPI_Allreduce(&localCount, &globalCount, 1, MPI_LONG_LONG, MPI_SUM, communicator);
return globalCount;
}
[[nodiscard]] mean_field::models::structure::StructureSeed make_n3_seed(null_space::Model &model) {
constexpr double surfaceCoordinate = 6.8968486193769603755;
constexpr int radialSampleCount = 8192;
const double pi = std::acos(-1.0);
const double radius = mean_field::utils::RADIUS;
const double targetMass = mean_field::utils::MASS;
constexpr double dimensionlessMass = 2.0182359509662283534;
const double polytropicConstant =
pi * mean_field::utils::G * std::pow(targetMass / (4.0 * pi * dimensionlessMass), 2.0 / 3.0);
const double centralDensity =
std::pow(surfaceCoordinate * std::sqrt(polytropicConstant / (pi * mean_field::utils::G)) / radius, 3.0);
return model.makeInitialSeed({.centralDensity = centralDensity, .radialSampleCount = radialSampleCount});
}
[[nodiscard]] mfem::Vector project_n3_density(
const mean_field::fem::FEM &fem,
const mean_field::models::structure::StructureSeed &seed
) {
const auto interpolate = [](const mfem::Vector &radii, const mfem::Vector &values, const double radius) {
if (radius <= radii(0)) {
return values(0);
}
const int finalIndex = radii.Size() - 1;
if (radius >= radii(finalIndex)) {
return values(finalIndex);
}
int lower = 0;
int upper = finalIndex;
while (upper - lower > 1) {
const int middle = lower + (upper - lower) / 2;
if (radii(middle) <= radius) {
lower = middle;
} else {
upper = middle;
}
}
const double fraction = (radius - radii(lower)) / (radii(upper) - radii(lower));
return (1.0 - fraction) * values(lower) + fraction * values(upper);
};
mfem::FunctionCoefficient densityCoefficient([&seed, &interpolate](const mfem::Vector &position) {
const double radius = position.Norml2();
return radius >= seed.stellarRadius ? 0.0 : interpolate(seed.radius, seed.density, radius);
});
mfem::ParGridFunction densityField(fem.densityFes.get());
densityField.ProjectCoefficient(densityCoefficient);
mfem::Vector densityTrue;
densityField.GetTrueDofs(densityTrue);
return densityTrue;
}
void run_homology_mass_cancellation_experiment(const int hRefinementLevel) {
REQUIRE(hRefinementLevel >= 0);
mean_field::utils::Args args = test_utils::setup_args();
mean_field::fem::FEM fem = mean_field::fem::setup_fem(args.mesh_file, args, hRefinementLevel);
REQUIRE(fem.okay());
null_space::Model model = null_space::make_model();
const mean_field::models::structure::StructureSeed seed = make_n3_seed(model);
const mfem::Vector densityTrue = project_n3_density(fem, seed);
auto deformation = model.compileDomainDeformation(fem);
const auto &surface = deformation.surfaceDeformationPrescription();
mfem::Vector zeroSurfaceParameters(surface.parameterCount());
mfem::Vector homologySurfaceDirection(surface.parameterCount());
zeroSurfaceParameters = 0.0;
for (int parameter = 0; parameter < homologySurfaceDirection.Size(); ++parameter) {
homologySurfaceDirection(parameter) = surface.referenceRadius(parameter);
}
mfem::Vector volumeDirection(deformation.volumeDisplacementSize());
deformation.applyJacobian(zeroSurfaceParameters, homologySurfaceDirection, volumeDirection);
mfem::ParGridFunction coordinateVelocity(fem.displacementFes.get());
coordinateVelocity.SetFromTrueDofs(volumeDirection);
mean_field::operators::context::gravity_field::GravityFieldLinearizationContext gravityContext(
fem, *fem.domainMapperStateless
);
const mean_field::field::FieldDofMap &densityMap = gravityContext.GetDensityMap();
const mfem::Vector reducedDensity = densityMap.gather(densityTrue);
const mfem::Vector densityDirection = project_extension_aware_homology_scalar(
*fem.densityFes, densityMap, reducedDensity, coordinateVelocity, surface.referenceCenter(), 3.0
);
mfem::Vector zeroDisplacement(fem.displacementFes->GetTrueVSize());
mfem::Vector zeroGravityGradient(fem.gravityFluxFes->GetTrueVSize());
mfem::Vector zeroGravityPotential(fem.gravityPotentialFes->GetTrueVSize());
zeroDisplacement = 0.0;
zeroGravityGradient = 0.0;
zeroGravityPotential = 0.0;
const mean_field::operators::MassNormalizationDependencies dependencies{
.discretization = {.identity = 9101, .revision = 1},
.density = {.identity = 9103, .revision = 1},
.displacement = {.identity = 9109, .revision = 1},
.targetMass = {.identity = 9127, .revision = 1}
};
gravityContext.Prepare(
{.density = reducedDensity,
.displacement = gravityContext.GetDisplacementMap().gather(zeroDisplacement),
.gravity_gradient = gravityContext.GetGravityGradientMap().gather(zeroGravityGradient),
.gravity_potential = gravityContext.GetGravityPotentialMap().gather(zeroGravityPotential)},
{.discretization = {.value = dependencies.discretization.revision},
.displacement = {.value = dependencies.displacement.revision},
.density = {.value = dependencies.density.revision},
.gravity_gradient = {.value = 1},
.gravity_potential = {.value = 1}}
);
mean_field::operators::PreparedMassNormalizationOperator massOperator(
fem, *fem.domainMapperStateless, gravityContext
);
massOperator.Prepare({.targetMass = mean_field::utils::MASS}, dependencies);
const mfem::Vector reducedVolumeDirection = gravityContext.GetDisplacementMap().gather(volumeDirection);
const HomologyMassCancellation cancellation =
measure_homology_mass_cancellation(massOperator, densityDirection, reducedVolumeDirection);
std::map<std::string, double> metrics{
{"density_direction_norm", null_space::global_norm(densityDirection, fem.mesh->GetComm())},
{"surface_direction_norm", null_space::global_norm(homologySurfaceDirection, fem.mesh->GetComm())},
{"volume_direction_norm", null_space::global_norm(volumeDirection, fem.mesh->GetComm())},
{"global_element_count", static_cast<double>(fem.mesh->GetGlobalNE())},
{"global_density_true_dof_count", static_cast<double>(fem.densityFes->GlobalTrueVSize())},
{"global_displacement_true_dof_count", static_cast<double>(fem.displacementFes->GlobalTrueVSize())},
{"global_surface_parameter_count", static_cast<double>(surface.globalParameterCount())}
};
add_homology_mass_metrics(metrics, cancellation);
int rank = 0;
MPI_Comm_rank(fem.mesh->GetComm(), &rank);
if (rank == 0) {
const int pRefinementLevel = mean_field::field::uniformPolynomialOrderIncrement;
experiment::record_experiment_result(
"n3_homology_mass_cancellation",
"h" + std::to_string(hRefinementLevel) + "_p" + std::to_string(pRefinementLevel),
{{"h_refinement_level", std::to_string(hRefinementLevel)},
{"p_refinement_level", std::to_string(pRefinementLevel)},
{"density_polynomial_order", std::to_string(mean_field::field::Density::Scalar::familyOrder)},
{"enthalpy_polynomial_order", std::to_string(mean_field::field::Enthalpy::Scalar::familyOrder)},
{"displacement_polynomial_order",
std::to_string(mean_field::field::Displacement::Vector::familyOrder)},
{"gravity_polynomial_order", std::to_string(mean_field::field::Gravity::Potential::familyOrder)},
{"mesh_file", test_utils::setup_args().mesh_file}},
std::move(metrics)
);
}
}
} // namespace
TEST_CASE(
"Coupled Stellar Equilibrium Homology And Reduced Surface Mode Responses",
"[null_space][surface_modes][conditioning][homology]"
) {
mean_field::utils::Args args = test_utils::setup_args();
args.p.rtol = std::min(args.p.rtol, 1.0e-12);
args.p.atol = std::min(args.p.atol, 1.0e-13);
args.p.max_iters = std::max(args.p.max_iters, 1500);
null_space::N3Equilibrium fixture(std::move(args));
const MPI_Comm communicator = fixture.fem().mesh->GetComm();
int rank = 0;
MPI_Comm_rank(communicator, &rank);
const std::vector<GaugeMode> modes = make_gauge_modes(fixture);
const auto &stellarOperator = fixture.stellar_operator();
const auto &layout = stellarOperator.GetLayout();
GravityUnknownJacobian gravityJacobian(stellarOperator);
mean_field::operators::ReducedGravityFieldPreconditioner gravityPreconditioner(
fixture.fem(), stellarOperator.GetGravityContext().GetGeometryContext()
);
mfem::MINRESSolver gravitySolver(communicator);
gravitySolver.SetOperator(gravityJacobian);
gravitySolver.SetPreconditioner(gravityPreconditioner);
gravitySolver.SetRelTol(1.0e-10);
gravitySolver.SetAbsTol(1.0e-12);
gravitySolver.SetMaxIter(1500);
gravitySolver.SetPrintLevel(0);
for (std::size_t modeIndex = 0; modeIndex < modes.size(); ++modeIndex) {
const GaugeMode &mode = modes[modeIndex];
null_space::report_progress(
communicator,
"evaluating " + mode.name + " (" + std::to_string(modeIndex + 1) + "/" + std::to_string(modes.size()) + ")"
);
const double prescribedInputNorm = null_space::global_norm(mode.direction, communicator);
REQUIRE(std::isfinite(prescribedInputNorm));
REQUIRE(prescribedInputNorm > 0.0);
const mfem::Vector prescribedAction = fixture.jacobian_action(mode.direction);
const double prescribedActionNorm = null_space::global_norm(prescribedAction, communicator);
GravityCompletionResult completion;
completion.direction.SetSize(gravityJacobian.Width());
completion.direction = 0.0;
mfem::Vector completedDirection(mode.direction);
if (mode.requiresGravityCompletion) {
completion =
solve_gravity_completion(prescribedAction, layout, communicator, gravityJacobian, gravitySolver);
assign_gravity_completion(
completedDirection, layout, completion.direction, gravityJacobian.gravity_gradient_size()
);
}
const mfem::Vector completedAction = fixture.jacobian_action(completedDirection);
const double completedInputNorm = null_space::global_norm(completedDirection, communicator);
const double completedActionNorm = null_space::global_norm(completedAction, communicator);
const double completionNorm = null_space::global_norm(completion.direction, communicator);
REQUIRE(std::isfinite(prescribedActionNorm));
REQUIRE(std::isfinite(completedInputNorm));
REQUIRE(std::isfinite(completedActionNorm));
REQUIRE(completedInputNorm > 0.0);
std::map<std::string, double> metrics{
{"prescribed_input_norm", prescribedInputNorm},
{"prescribed_action_norm", prescribedActionNorm},
{"completed_input_norm", completedInputNorm},
{"completed_action_norm", completedActionNorm},
{"normalized_completed_response", completedActionNorm / completedInputNorm},
{"gravity_completion_norm", completionNorm},
{"gravity_solve_rhs_norm", completion.rightHandSideNorm},
{"gravity_solve_residual_norm", completion.residualNorm},
{"gravity_solve_relative_residual", completion.relativeResidual},
{"gravity_solve_final_norm", completion.finalNorm},
{"gravity_solve_iterations", static_cast<double>(completion.iterations)},
{"global_nonzero_input_dofs", static_cast<double>(global_nonzero_count(mode.direction, communicator))}
};
if (mode.family == "homology") {
const mfem::Vector densityDirection =
null_space::const_value_view(mode.direction, layout, null_space::densityValue);
const mfem::Vector volumeDirection = fixture.lifted_surface_direction(mode.direction);
const HomologyMassCancellation massCancellation = measure_homology_mass_cancellation(
stellarOperator.GetMassNormalizationOperator(), densityDirection, volumeDirection
);
add_homology_mass_metrics(metrics, massCancellation);
}
if (completedActionNorm > 0.0) {
metrics.emplace("gravity_completion_reduction", prescribedActionNorm / completedActionNorm);
}
add_block_metrics(
metrics, "prescribed_", null_space::residual_block_norms(prescribedAction, layout, communicator)
);
add_block_metrics(
metrics, "completed_", null_space::residual_block_norms(completedAction, layout, communicator)
);
if (rank == 0) {
experiment::record_experiment_result(
"coupled_reduced_surface_mode_conditioning", mode.name,
{{"mode_family", mode.family},
{"axis", std::to_string(mode.axis)},
{"gravity_completion_requested", mode.requiresGravityCompletion ? "true" : "false"},
{"gravity_solve_performed", completion.solvePerformed ? "true" : "false"},
{"rotation_fraction_of_keplerian", "0.0"},
{"mesh_file", test_utils::setup_args().mesh_file},
{"local_state_dofs", std::to_string(stellarOperator.Width())}},
std::move(metrics)
);
}
}
null_space::report_progress(communicator, "coupled reduced surface-mode probe complete; writing CSV output");
}
TEST_CASE(
"N3 Homology Mass Cancellation At The Registered Polynomial Order",
"[null_space][homology][mass_normalization][convergence][p_refinement]"
) {
run_homology_mass_cancellation_experiment(0);
}
TEST_CASE(
"N3 Homology Mass Cancellation Under Uniform Spatial Refinement",
"[null_space][homology][mass_normalization][convergence][h_refinement]"
) {
run_homology_mass_cancellation_experiment(1);
}

View File

@@ -6,6 +6,7 @@
#include <fourdst/config/config.h>
#include <mfem.hpp>
#include <catch2/catch_test_case_info.hpp>
#include <fstream>
#include <iomanip>
#include <iostream>
@@ -14,8 +15,6 @@
#include <string>
#include <string_view>
#include <vector>
#include <catch2/catch_test_case_info.hpp>
import mean_field;
import test_helpers;
@@ -107,10 +106,8 @@ public:
void testCaseEnded(const Catch::TestCaseStats &statistics) override {
StreamingReporterBase::testCaseEnded(statistics);
const bool passed = statistics.totals.assertions.allPassed();
std::cout << (passed ? "PASS " : "FAIL ")
<< statistics.testInfo->name
<< " (" << statistics.totals.assertions.passed
<< " assertions)\n";
std::cout << (passed ? "PASS " : "FAIL ") << statistics.testInfo->name << " ("
<< statistics.totals.assertions.passed << " assertions)\n";
}
void testRunEnded(const Catch::TestRunStats &statistics) override {
@@ -119,9 +116,15 @@ public:
}
};
CATCH_REGISTER_REPORTER("experiment", ExperimentReporter)
CATCH_REGISTER_REPORTER(
"experiment",
ExperimentReporter
)
int main(int argc, char* argv[]) {
int main(
int argc,
char *argv[]
) {
fourdst::config::Config<mean_field::utils::Args> config;
CLI::App app{"Mean Field accuracy experiments"};
@@ -171,8 +174,8 @@ int main(int argc, char* argv[]) {
bool has_reporter = false;
for (const std::string &argument : catch_arguments) {
has_reporter = has_reporter || argument == "-r" || argument == "--reporter" ||
argument.starts_with("-r=") || argument.starts_with("--reporter=");
has_reporter = has_reporter || argument == "-r" || argument == "--reporter" || argument.starts_with("-r=") ||
argument.starts_with("--reporter=");
}
if (!has_reporter) {
catch_arguments.emplace_back("--reporter");

View File

@@ -52,14 +52,18 @@ export namespace experiment {
inline void record_experiment_result(
const std::string &experiment_name,
const std::string &case_name,
std::map<std::string, std::string> parameters,
std::map<std::string, double> metrics
std::map<
std::string,
std::string> parameters,
std::map<
std::string,
double> metrics
) {
ExperimentRegistry::instance().add_result({
.experiment_name = experiment_name,
ExperimentRegistry::instance().add_result(
{.experiment_name = experiment_name,
.case_name = case_name,
.parameters = std::move(parameters),
.metrics = std::move(metrics)
});
}
.metrics = std::move(metrics)}
);
}
} // namespace experiment

View File

@@ -34,31 +34,35 @@ struct AccuracyBudgetMetrics {
double virial_consistency_error{0.0};
};
static double global_norm(const mfem::Vector &vector, MPI_Comm communicator) {
static double global_norm(
const mfem::Vector &vector,
MPI_Comm communicator
) {
const double local_norm_squared = vector * vector;
double global_norm_squared = 0.0;
MPI_Allreduce(&local_norm_squared, &global_norm_squared, 1, MPI_DOUBLE,
MPI_SUM, communicator);
MPI_Allreduce(&local_norm_squared, &global_norm_squared, 1, MPI_DOUBLE, MPI_SUM, communicator);
return std::sqrt(global_norm_squared);
}
static double global_dot(const mfem::Vector &left, const mfem::Vector &right,
MPI_Comm communicator) {
static double global_dot(
const mfem::Vector &left,
const mfem::Vector &right,
MPI_Comm communicator
) {
const double local_dot = left * right;
double global_dot_product = 0.0;
MPI_Allreduce(&local_dot, &global_dot_product, 1, MPI_DOUBLE, MPI_SUM,
communicator);
MPI_Allreduce(&local_dot, &global_dot_product, 1, MPI_DOUBLE, MPI_SUM, communicator);
return global_dot_product;
}
static void zero_vacuum_density(const mean_field::fem::FEM &fem,
mfem::GridFunction &density) {
using DomainSchema =
mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
static void zero_vacuum_density(
const mean_field::fem::FEM &fem,
mfem::GridFunction &density
) {
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
const mean_field::field::FieldDofMap density_map =
mean_field::field::make_field_dof_map<mean_field::field::Density,
DomainSchema>(*fem.densityFes);
mean_field::field::make_field_dof_map<mean_field::field::Density, DomainSchema>(*fem.densityFes);
mfem::Vector density_true;
density.GetTrueDofs(density_true);
@@ -69,14 +73,15 @@ static void zero_vacuum_density(const mean_field::fem::FEM &fem,
}
static int diagnostic_quadrature_order(const mean_field::fem::FEM &fem) {
return 2 * std::max(fem.gravityPotentialFes->GetMaxElementOrder(),
fem.gravityFluxFes->GetMaxElementOrder()) +
8;
return 2 * std::max(fem.gravityPotentialFes->GetMaxElementOrder(), fem.gravityFluxFes->GetMaxElementOrder()) + 8;
}
static mfem::Vector assemble_monopole_projection_rhs(
mean_field::fem::FEM &fem, const mfem::GridFunction &displacement,
const double mass, const double stellar_radius) {
mean_field::fem::FEM &fem,
const mfem::GridFunction &displacement,
const double mass,
const double stellar_radius
) {
*fem.displacement = displacement;
mfem::Vector local_rhs(fem.gravityFluxFes->GetVSize());
@@ -85,18 +90,15 @@ static mfem::Vector assemble_monopole_projection_rhs(
const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute;
const int quadrature_order = diagnostic_quadrature_order(fem);
mean_field::mapping::GridFunctionMappingEvaluator mapping_evaluator(
*fem.domainMapperStateless, *fem.displacement,
*fem.compactificationCoordinate);
*fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate
);
for (int element_id = 0; element_id < fem.mesh->GetNE(); ++element_id) {
const mfem::FiniteElement &gravity_element =
*fem.gravityFluxFes->GetFE(element_id);
mfem::ElementTransformation *transformation =
fem.mesh->GetElementTransformation(element_id);
const mfem::FiniteElement &gravity_element = *fem.gravityFluxFes->GetFE(element_id);
mfem::ElementTransformation *transformation = fem.mesh->GetElementTransformation(element_id);
mfem::Array<int> gravity_dofs;
mfem::DofTransformation *gravity_transform =
fem.gravityFluxFes->GetElementVDofs(element_id, gravity_dofs);
mfem::DofTransformation *gravity_transform = fem.gravityFluxFes->GetElementVDofs(element_id, gravity_dofs);
const int dof_count = gravity_element.GetDof();
const int dimension = transformation->GetSpaceDim();
@@ -107,35 +109,29 @@ static mfem::Vector assemble_monopole_projection_rhs(
mfem::DenseMatrix vector_shape(dof_count, dimension);
element_rhs = 0.0;
const mfem::IntegrationRule &rule =
mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order);
const mfem::IntegrationRule &rule = mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order);
for (int quadrature_point_id = 0; quadrature_point_id < rule.GetNPoints();
++quadrature_point_id) {
for (int quadrature_point_id = 0; quadrature_point_id < rule.GetNPoints(); ++quadrature_point_id) {
const mfem::IntegrationPoint &point = rule.IntPoint(quadrature_point_id);
mean_field::mapping::MappingPointContext mapping_context;
MFEM_VERIFY(
mapping_evaluator.EvaluatePoint(*transformation, point,
mapping_context) ==
mapping_evaluator.EvaluatePoint(*transformation, point, mapping_context) ==
mean_field::mapping::MappingStatus::valid,
"Invalid mapping in monopole projection RHS.");
"Invalid mapping in monopole projection RHS."
);
physical_position = mapping_context.physical_position;
const double radius = physical_position.Norml2();
MFEM_VERIFY(std::isfinite(radius) && radius > 0.0,
"Invalid radius in monopole projection RHS.");
MFEM_VERIFY(std::isfinite(radius) && radius > 0.0, "Invalid radius in monopole projection RHS.");
analytic_field = physical_position;
if (transformation->Attribute == vacuum_attribute) {
analytic_field *=
mean_field::utils::G * mass / (radius * radius * radius);
analytic_field *= mean_field::utils::G * mass / (radius * radius * radius);
} else {
analytic_field *= mean_field::utils::G * mass /
(stellar_radius * stellar_radius * stellar_radius);
analytic_field *= mean_field::utils::G * mass / (stellar_radius * stellar_radius * stellar_radius);
}
mapping_context.mapping_jacobian.MultTranspose(analytic_field,
pulled_field);
mapping_context.mapping_jacobian.MultTranspose(analytic_field, pulled_field);
transformation->SetIntPoint(&point);
gravity_element.CalcVShape(*transformation, vector_shape);
@@ -143,8 +139,7 @@ static mfem::Vector assemble_monopole_projection_rhs(
for (int dof = 0; dof < dof_count; ++dof) {
for (int component = 0; component < dimension; ++component) {
element_rhs(dof) += reference_weight * vector_shape(dof, component) *
pulled_field(component);
element_rhs(dof) += reference_weight * vector_shape(dof, component) * pulled_field(component);
}
}
}
@@ -157,8 +152,7 @@ static mfem::Vector assemble_monopole_projection_rhs(
mfem::Vector true_rhs(fem.gravityFluxFes->GetTrueVSize());
true_rhs = 0.0;
const mfem::Operator *prolongation =
fem.gravityFluxFes->GetProlongationMatrix();
const mfem::Operator *prolongation = fem.gravityFluxFes->GetProlongationMatrix();
if (prolongation != nullptr) {
prolongation->MultTranspose(local_rhs, true_rhs);
} else {
@@ -168,23 +162,21 @@ static mfem::Vector assemble_monopole_projection_rhs(
return true_rhs;
}
static mfem::Vector
project_monopole_gradient(mean_field::fem::FEM &fem,
static mfem::Vector project_monopole_gradient(
mean_field::fem::FEM &fem,
const mfem::GridFunction &displacement,
const double mass, const double stellar_radius) {
const double mass,
const double stellar_radius
) {
mfem::Vector displacement_true;
displacement.GetTrueDofs(displacement_true);
const mfem::Vector projection_rhs_true =
assemble_monopole_projection_rhs(fem, displacement, mass, stellar_radius);
const mfem::Vector projection_rhs_true = assemble_monopole_projection_rhs(fem, displacement, mass, stellar_radius);
mean_field::operators::PreparedMappedHDivMassOperator mass_operator(
fem, *fem.domainMapperStateless);
mass_operator.Prepare(
mass_operator.GetDisplacementMap().gather(displacement_true));
mean_field::operators::PreparedMappedHDivMassOperator mass_operator(fem, *fem.domainMapperStateless);
mass_operator.Prepare(mass_operator.GetDisplacementMap().gather(displacement_true));
const mfem::Vector projection_rhs =
mass_operator.GetFluxMap().gather(projection_rhs_true);
const mfem::Vector projection_rhs = mass_operator.GetFluxMap().gather(projection_rhs_true);
mfem::CGSolver solver(fem.gravityFluxFes->GetComm());
solver.SetOperator(mass_operator);
@@ -193,8 +185,7 @@ project_monopole_gradient(mean_field::fem::FEM &fem,
solver.SetMaxIter(4000);
solver.SetPrintLevel(0);
mfem::Vector projected_gradient_reduced(
mass_operator.GetFluxMap().reduced_size());
mfem::Vector projected_gradient_reduced(mass_operator.GetFluxMap().reduced_size());
projected_gradient_reduced = 0.0;
solver.Mult(projection_rhs, projected_gradient_reduced);
@@ -204,56 +195,51 @@ project_monopole_gradient(mean_field::fem::FEM &fem,
const double relative_residual =
global_norm(residual, fem.gravityFluxFes->GetComm()) /
std::max(global_norm(projection_rhs, fem.gravityFluxFes->GetComm()),
std::numeric_limits<double>::epsilon());
std::max(global_norm(projection_rhs, fem.gravityFluxFes->GetComm()), std::numeric_limits<double>::epsilon());
REQUIRE(std::isfinite(relative_residual));
REQUIRE(relative_residual < 1.0e-8);
return mass_operator.GetFluxMap().scatter(projected_gradient_reduced);
}
static double mapped_hdiv_relative_gap(mean_field::fem::FEM &fem,
static double mapped_hdiv_relative_gap(
mean_field::fem::FEM &fem,
const mfem::GridFunction &displacement,
const mfem::Vector &calculated,
const mfem::Vector &reference) {
const mfem::Vector &reference
) {
mfem::Vector displacement_true;
displacement.GetTrueDofs(displacement_true);
mean_field::operators::PreparedMappedHDivMassOperator mass_operator(
fem, *fem.domainMapperStateless);
mass_operator.Prepare(
mass_operator.GetDisplacementMap().gather(displacement_true));
mean_field::operators::PreparedMappedHDivMassOperator mass_operator(fem, *fem.domainMapperStateless);
mass_operator.Prepare(mass_operator.GetDisplacementMap().gather(displacement_true));
mfem::Vector difference(calculated);
difference -= reference;
mfem::Vector difference_action;
mfem::Vector reference_action;
const mfem::Vector reduced_difference =
mass_operator.GetFluxMap().gather(difference);
const mfem::Vector reduced_reference =
mass_operator.GetFluxMap().gather(reference);
const mfem::Vector reduced_difference = mass_operator.GetFluxMap().gather(difference);
const mfem::Vector reduced_reference = mass_operator.GetFluxMap().gather(reference);
mass_operator.Mult(reduced_difference, difference_action);
mass_operator.Mult(reduced_reference, reference_action);
const double difference_energy = global_dot(
reduced_difference, difference_action, fem.gravityFluxFes->GetComm());
const double reference_energy = global_dot(
reduced_reference, reference_action, fem.gravityFluxFes->GetComm());
MFEM_VERIFY(reference_energy > 0.0,
"Projected monopole field has zero mapped H(div) norm.");
const double difference_energy = global_dot(reduced_difference, difference_action, fem.gravityFluxFes->GetComm());
const double reference_energy = global_dot(reduced_reference, reference_action, fem.gravityFluxFes->GetComm());
MFEM_VERIFY(reference_energy > 0.0, "Projected monopole field has zero mapped H(div) norm.");
return std::sqrt(std::max(0.0, difference_energy) / reference_energy);
}
static AccuracyBudgetEnergies
measure_stellar_energies(mean_field::fem::FEM &fem,
static AccuracyBudgetEnergies measure_stellar_energies(
mean_field::fem::FEM &fem,
const mfem::GridFunction &density,
const mean_field::physics::GravitySolution &solution) {
const mean_field::physics::GravitySolution &solution
) {
const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute;
const int quadrature_order = diagnostic_quadrature_order(fem);
mean_field::mapping::GridFunctionMappingEvaluator mapping_evaluator(
*fem.domainMapperStateless, *fem.displacement,
*fem.compactificationCoordinate);
*fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate
);
double local_binding = 0.0;
double local_virial = 0.0;
@@ -262,40 +248,33 @@ measure_stellar_energies(mean_field::fem::FEM &fem,
mfem::Vector physical_field(3);
for (int element_id = 0; element_id < fem.mesh->GetNE(); ++element_id) {
mfem::ElementTransformation *transformation =
fem.mesh->GetElementTransformation(element_id);
mfem::ElementTransformation *transformation = fem.mesh->GetElementTransformation(element_id);
if (transformation->Attribute == vacuum_attribute) {
continue;
}
const mfem::IntegrationRule &rule =
mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order);
const mfem::IntegrationRule &rule = mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order);
for (int quadrature_point_id = 0; quadrature_point_id < rule.GetNPoints();
++quadrature_point_id) {
for (int quadrature_point_id = 0; quadrature_point_id < rule.GetNPoints(); ++quadrature_point_id) {
const mfem::IntegrationPoint &point = rule.IntPoint(quadrature_point_id);
transformation->SetIntPoint(&point);
mean_field::mapping::MappingPointContext mapping_context;
MFEM_VERIFY(
mapping_evaluator.EvaluatePoint(*transformation, point,
mapping_context) ==
mapping_evaluator.EvaluatePoint(*transformation, point, mapping_context) ==
mean_field::mapping::MappingStatus::valid,
"Invalid mapping in energy diagnostic.");
"Invalid mapping in energy diagnostic."
);
physical_position = mapping_context.physical_position;
const mfem::DenseMatrix &mapping_jacobian =
mapping_context.mapping_jacobian;
const double mapping_determinant =
mapping_context.mapping_determinant;
MFEM_VERIFY(mapping_determinant > 0.0,
"Non-positive mapping determinant in energy diagnostic.");
const mfem::DenseMatrix &mapping_jacobian = mapping_context.mapping_jacobian;
const double mapping_determinant = mapping_context.mapping_determinant;
MFEM_VERIFY(mapping_determinant > 0.0, "Non-positive mapping determinant in energy diagnostic.");
solution.gradPhi.GetVectorValue(element_id, point, reference_field);
mapping_jacobian.Mult(reference_field, physical_field);
physical_field /= mapping_determinant;
const double weight =
point.weight * transformation->Weight() * mapping_determinant;
const double weight = point.weight * transformation->Weight() * mapping_determinant;
const double rho = density.GetValue(element_id, point);
const double phi = solution.phi.GetValue(element_id, point);
local_binding += 0.5 * rho * phi * weight;
@@ -304,49 +283,44 @@ measure_stellar_energies(mean_field::fem::FEM &fem,
}
AccuracyBudgetEnergies energies;
MPI_Allreduce(&local_binding, &energies.binding, 1, MPI_DOUBLE, MPI_SUM,
fem.densityFes->GetComm());
MPI_Allreduce(&local_virial, &energies.virial, 1, MPI_DOUBLE, MPI_SUM,
fem.densityFes->GetComm());
MPI_Allreduce(&local_binding, &energies.binding, 1, MPI_DOUBLE, MPI_SUM, fem.densityFes->GetComm());
MPI_Allreduce(&local_virial, &energies.virial, 1, MPI_DOUBLE, MPI_SUM, fem.densityFes->GetComm());
return energies;
}
static double reduced_gravity_relative_residual(
mean_field::fem::FEM &fem, const mfem::GridFunction &density,
mean_field::fem::FEM &fem,
const mfem::GridFunction &density,
const mfem::GridFunction &displacement,
const mean_field::physics::GravitySolution &solution) {
const mean_field::physics::GravitySolution &solution
) {
using GravityFieldForm = mean_field::utils::blocks::gravity_field_form;
constexpr auto gradient_block =
mean_field::utils::blocks::get_residual_block<GravityFieldForm>(
mean_field::utils::blocks::gravity_field.gradient_term);
constexpr auto poisson_block =
mean_field::utils::blocks::get_residual_block<GravityFieldForm>(
mean_field::utils::blocks::gravity_field.poisson_term);
constexpr auto gradient_block = mean_field::utils::blocks::get_residual_block<GravityFieldForm>(
mean_field::utils::blocks::gravity_field.gradient_term
);
constexpr auto poisson_block = mean_field::utils::blocks::get_residual_block<GravityFieldForm>(
mean_field::utils::blocks::gravity_field.poisson_term
);
using DomainSchema =
mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
const mean_field::field::FieldDofMap density_map =
mean_field::field::make_field_dof_map<mean_field::field::Density,
DomainSchema>(*fem.densityFes);
mean_field::field::make_field_dof_map<mean_field::field::Density, DomainSchema>(*fem.densityFes);
const mean_field::field::FieldDofMap displacement_map =
mean_field::field::make_field_dof_map<mean_field::field::Displacement,
DomainSchema>(*fem.displacementFes);
mean_field::field::make_field_dof_map<mean_field::field::Displacement, DomainSchema>(*fem.displacementFes);
const mean_field::field::FieldDofMap gravity_flux_map =
mean_field::field::make_field_dof_map<mean_field::field::Gravity,
DomainSchema>(*fem.gravityFluxFes);
mean_field::field::make_field_dof_map<mean_field::field::Gravity, DomainSchema>(*fem.gravityFluxFes);
const mean_field::field::FieldDofMap gravity_potential_map =
mean_field::field::make_field_dof_map<mean_field::field::Gravity,
DomainSchema>(
*fem.gravityPotentialFes);
mean_field::field::make_field_dof_map<mean_field::field::Gravity, DomainSchema>(*fem.gravityPotentialFes);
const std::array<int, GravityFieldForm::value_block_count> value_sizes{
density_map.reduced_size(), displacement_map.reduced_size(),
gravity_flux_map.reduced_size(), gravity_potential_map.reduced_size()};
density_map.reduced_size(), displacement_map.reduced_size(), gravity_flux_map.reduced_size(),
gravity_potential_map.reduced_size()
};
const std::array<int, GravityFieldForm::residual_block_count> residual_sizes{
gravity_flux_map.reduced_size(), gravity_potential_map.reduced_size()};
const mean_field::utils::blocks::form_layout<GravityFieldForm> layout(
value_sizes, residual_sizes);
gravity_flux_map.reduced_size(), gravity_potential_map.reduced_size()
};
const mean_field::utils::blocks::form_layout<GravityFieldForm> layout(value_sizes, residual_sizes);
mfem::Vector density_true;
mfem::Vector displacement_true;
@@ -357,24 +331,24 @@ static double reduced_gravity_relative_residual(
solution.gradPhi.GetTrueDofs(gradient_true);
solution.phi.GetTrueDofs(potential_true);
mean_field::operators::context::gravity_field::
GravityFieldLinearizationContext linearization_context(
fem, *fem.domainMapperStateless);
mean_field::operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(
fem, *fem.domainMapperStateless
);
mean_field::operators::GravityFieldJacobianOperator jacobian(
fem, *fem.domainMapperStateless, linearization_context,
layout.value_offsets(), layout.residual_offsets());
fem, *fem.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()
);
mean_field::operators::GravityFieldOperator field_operator(
fem, *fem.domainMapperStateless, linearization_context,
layout.value_offsets(), jacobian);
mean_field::operators::context::gravity_field::GravityFieldGeometryContext
geometry_context(fem, *fem.domainMapperStateless);
fem, *fem.domainMapperStateless, linearization_context, layout.value_offsets(), jacobian
);
mean_field::operators::context::gravity_field::GravityFieldGeometryContext geometry_context(
fem, *fem.domainMapperStateless
);
mean_field::operators::ReducedGravityFieldOperator reduced_operator(
field_operator, geometry_context,
displacement_map.gather(displacement_true));
field_operator, geometry_context, displacement_map.gather(displacement_true)
);
mfem::Vector right_hand_side;
reduced_operator.BuildRightHandSide(density_map.gather(density_true),
right_hand_side);
reduced_operator.BuildRightHandSide(density_map.gather(density_true), right_hand_side);
mfem::BlockVector state(layout.residual_offsets());
state = 0.0;
@@ -386,18 +360,19 @@ static double reduced_gravity_relative_residual(
residual -= right_hand_side;
return global_norm(residual, fem.mesh->GetComm()) /
std::max(global_norm(right_hand_side, fem.mesh->GetComm()),
std::numeric_limits<double>::epsilon());
std::max(global_norm(right_hand_side, fem.mesh->GetComm()), std::numeric_limits<double>::epsilon());
}
static AccuracyBudgetMetrics
measure_monopole_accuracy(mean_field::fem::FEM &fem,
static AccuracyBudgetMetrics measure_monopole_accuracy(
mean_field::fem::FEM &fem,
const mfem::GridFunction &density,
const mfem::GridFunction &displacement,
const mean_field::physics::GravitySolution &solution,
const mfem::ParGridFunction &projected_potential,
const mfem::Vector &projected_gradient,
const double mass, const double stellar_radius) {
const double mass,
const double stellar_radius
) {
mfem::Vector solution_gradient;
solution.gradPhi.GetTrueDofs(solution_gradient);
@@ -406,8 +381,7 @@ measure_monopole_accuracy(mean_field::fem::FEM &fem,
solution.phi.GetTrueDofs(solution_potential);
projected_potential.GetTrueDofs(projection_potential);
mfem::ParGridFunction projected_gradient_grid_function(
fem.gravityFluxFes.get());
mfem::ParGridFunction projected_gradient_grid_function(fem.gravityFluxFes.get());
projected_gradient_grid_function.SetFromTrueDofs(projected_gradient);
double local_solution_gradient_error = 0.0;
@@ -420,8 +394,8 @@ measure_monopole_accuracy(mean_field::fem::FEM &fem,
const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute;
const int quadrature_order = diagnostic_quadrature_order(fem);
mean_field::mapping::GridFunctionMappingEvaluator mapping_evaluator(
*fem.domainMapperStateless, *fem.displacement,
*fem.compactificationCoordinate);
*fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate
);
mfem::Vector physical_position(3);
mfem::Vector analytic_gradient(3);
mfem::Vector solution_reference_gradient(3);
@@ -430,154 +404,118 @@ measure_monopole_accuracy(mean_field::fem::FEM &fem,
mfem::Vector projection_physical_gradient(3);
for (int element_id = 0; element_id < fem.mesh->GetNE(); ++element_id) {
mfem::ElementTransformation *transformation =
fem.mesh->GetElementTransformation(element_id);
const mfem::IntegrationRule &rule =
mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order);
mfem::ElementTransformation *transformation = fem.mesh->GetElementTransformation(element_id);
const mfem::IntegrationRule &rule = mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order);
for (int quadrature_point_id = 0; quadrature_point_id < rule.GetNPoints();
++quadrature_point_id) {
for (int quadrature_point_id = 0; quadrature_point_id < rule.GetNPoints(); ++quadrature_point_id) {
const mfem::IntegrationPoint &point = rule.IntPoint(quadrature_point_id);
transformation->SetIntPoint(&point);
mean_field::mapping::MappingPointContext mapping_context;
MFEM_VERIFY(
mapping_evaluator.EvaluatePoint(*transformation, point,
mapping_context) ==
mapping_evaluator.EvaluatePoint(*transformation, point, mapping_context) ==
mean_field::mapping::MappingStatus::valid,
"Invalid mapping in accuracy diagnostic.");
"Invalid mapping in accuracy diagnostic."
);
physical_position = mapping_context.physical_position;
const mfem::DenseMatrix &mapping_jacobian =
mapping_context.mapping_jacobian;
const double mapping_determinant =
mapping_context.mapping_determinant;
MFEM_VERIFY(mapping_determinant > 0.0,
"Non-positive mapping determinant in accuracy diagnostic.");
const mfem::DenseMatrix &mapping_jacobian = mapping_context.mapping_jacobian;
const double mapping_determinant = mapping_context.mapping_determinant;
MFEM_VERIFY(mapping_determinant > 0.0, "Non-positive mapping determinant in accuracy diagnostic.");
const double radius = physical_position.Norml2();
MFEM_VERIFY(std::isfinite(radius) && radius > 0.0,
"Invalid radius in monopole diagnostic.");
MFEM_VERIFY(std::isfinite(radius) && radius > 0.0, "Invalid radius in monopole diagnostic.");
analytic_gradient = physical_position;
double analytic_potential = 0.0;
if (transformation->Attribute == vacuum_attribute) {
analytic_gradient *=
mean_field::utils::G * mass / (radius * radius * radius);
analytic_gradient *= mean_field::utils::G * mass / (radius * radius * radius);
analytic_potential = -mean_field::utils::G * mass / radius;
} else {
analytic_gradient *= mean_field::utils::G * mass /
(stellar_radius * stellar_radius * stellar_radius);
analytic_potential =
-mean_field::utils::G * mass *
analytic_gradient *= mean_field::utils::G * mass / (stellar_radius * stellar_radius * stellar_radius);
analytic_potential = -mean_field::utils::G * mass *
(3.0 * stellar_radius * stellar_radius - radius * radius) /
(2.0 * stellar_radius * stellar_radius * stellar_radius);
}
solution.gradPhi.GetVectorValue(element_id, point,
solution_reference_gradient);
mapping_jacobian.Mult(solution_reference_gradient,
solution_physical_gradient);
solution.gradPhi.GetVectorValue(element_id, point, solution_reference_gradient);
mapping_jacobian.Mult(solution_reference_gradient, solution_physical_gradient);
solution_physical_gradient /= mapping_determinant;
projected_gradient_grid_function.GetVectorValue(
element_id, point, projection_reference_gradient);
mapping_jacobian.Mult(projection_reference_gradient,
projection_physical_gradient);
projected_gradient_grid_function.GetVectorValue(element_id, point, projection_reference_gradient);
mapping_jacobian.Mult(projection_reference_gradient, projection_physical_gradient);
projection_physical_gradient /= mapping_determinant;
const double solution_potential_value =
solution.phi.GetValue(element_id, point);
const double projection_potential_value =
projected_potential.GetValue(element_id, point);
const double weight =
point.weight * transformation->Weight() * mapping_determinant;
const double solution_potential_value = solution.phi.GetValue(element_id, point);
const double projection_potential_value = projected_potential.GetValue(element_id, point);
const double weight = point.weight * transformation->Weight() * mapping_determinant;
solution_physical_gradient -= analytic_gradient;
projection_physical_gradient -= analytic_gradient;
local_solution_gradient_error +=
weight * (solution_physical_gradient * solution_physical_gradient);
local_projection_gradient_error +=
weight *
(projection_physical_gradient * projection_physical_gradient);
local_solution_gradient_error += weight * (solution_physical_gradient * solution_physical_gradient);
local_projection_gradient_error += weight * (projection_physical_gradient * projection_physical_gradient);
local_gradient_norm += weight * (analytic_gradient * analytic_gradient);
local_solution_potential_error +=
weight * (solution_potential_value - analytic_potential) *
local_solution_potential_error += weight * (solution_potential_value - analytic_potential) *
(solution_potential_value - analytic_potential);
local_projection_potential_error +=
weight * (projection_potential_value - analytic_potential) *
local_projection_potential_error += weight * (projection_potential_value - analytic_potential) *
(projection_potential_value - analytic_potential);
local_potential_norm += weight * analytic_potential * analytic_potential;
}
}
const std::array<double, 6> local_values{local_solution_gradient_error,
local_projection_gradient_error,
local_gradient_norm,
local_solution_potential_error,
local_projection_potential_error,
local_potential_norm};
const std::array<double, 6> local_values{
local_solution_gradient_error, local_projection_gradient_error, local_gradient_norm,
local_solution_potential_error, local_projection_potential_error, local_potential_norm
};
std::array<double, 6> global_values{};
MPI_Allreduce(local_values.data(), global_values.data(),
static_cast<int>(local_values.size()), MPI_DOUBLE, MPI_SUM,
fem.mesh->GetComm());
MPI_Allreduce(
local_values.data(), global_values.data(), static_cast<int>(local_values.size()), MPI_DOUBLE, MPI_SUM,
fem.mesh->GetComm()
);
const AccuracyBudgetEnergies energies =
measure_stellar_energies(fem, density, solution);
const double analytic_energy =
-3.0 * mean_field::utils::G * mass * mass / (5.0 * stellar_radius);
const AccuracyBudgetEnergies energies = measure_stellar_energies(fem, density, solution);
const double analytic_energy = -3.0 * mean_field::utils::G * mass * mass / (5.0 * stellar_radius);
REQUIRE(global_values[2] > 0.0);
REQUIRE(global_values[5] > 0.0);
AccuracyBudgetMetrics metrics;
metrics.direct_relative_residual =
reduced_gravity_relative_residual(fem, density, displacement, solution);
metrics.gradient_relative_error =
std::sqrt(global_values[0] / global_values[2]);
metrics.gradient_projection_relative_error =
std::sqrt(global_values[1] / global_values[2]);
metrics.gradient_solution_projection_gap = mapped_hdiv_relative_gap(
fem, displacement, solution_gradient, projected_gradient);
metrics.potential_relative_error =
std::sqrt(global_values[3] / global_values[5]);
metrics.potential_projection_relative_error =
std::sqrt(global_values[4] / global_values[5]);
metrics.direct_relative_residual = reduced_gravity_relative_residual(fem, density, displacement, solution);
metrics.gradient_relative_error = std::sqrt(global_values[0] / global_values[2]);
metrics.gradient_projection_relative_error = std::sqrt(global_values[1] / global_values[2]);
metrics.gradient_solution_projection_gap =
mapped_hdiv_relative_gap(fem, displacement, solution_gradient, projected_gradient);
metrics.potential_relative_error = std::sqrt(global_values[3] / global_values[5]);
metrics.potential_projection_relative_error = std::sqrt(global_values[4] / global_values[5]);
mfem::Vector potential_difference(solution_potential);
potential_difference -= projection_potential;
const double projection_potential_norm =
global_norm(projection_potential, fem.gravityPotentialFes->GetComm());
const double projection_potential_norm = global_norm(projection_potential, fem.gravityPotentialFes->GetComm());
REQUIRE(projection_potential_norm > 0.0);
metrics.potential_solution_projection_gap =
global_norm(potential_difference, fem.gravityPotentialFes->GetComm()) /
projection_potential_norm;
metrics.binding_relative_error =
std::abs(energies.binding - analytic_energy) / std::abs(analytic_energy);
metrics.virial_relative_error =
std::abs(energies.virial - analytic_energy) / std::abs(analytic_energy);
metrics.virial_consistency_error =
std::abs(energies.binding - energies.virial) /
std::max(std::abs(energies.binding),
std::numeric_limits<double>::epsilon());
global_norm(potential_difference, fem.gravityPotentialFes->GetComm()) / projection_potential_norm;
metrics.binding_relative_error = std::abs(energies.binding - analytic_energy) / std::abs(analytic_energy);
metrics.virial_relative_error = std::abs(energies.virial - analytic_energy) / std::abs(analytic_energy);
metrics.virial_consistency_error = std::abs(energies.binding - energies.virial) /
std::max(std::abs(energies.binding), std::numeric_limits<double>::epsilon());
return metrics;
}
static void run_monopole_case(const std::string &sweep_name,
static void run_monopole_case(
const std::string &sweep_name,
const std::string &case_name,
mean_field::utils::Args args,
const double solver_tolerance,
const int quadrature_boost) {
const int quadrature_boost
) {
args.p.rtol = solver_tolerance;
args.p.atol = std::min(args.p.atol, solver_tolerance * 1.0e-2);
args.p.max_iters = std::max(args.p.max_iters, 2000);
args.quadrature.global_boost = quadrature_boost;
mean_field::fem::FEM fem =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
mean_field::fem::FEM fem = mean_field::fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(fem.domainMapperStateless != nullptr);
const double stellar_radius = mean_field::utils::RADIUS;
const double mass = mean_field::utils::MASS;
const double density_value = mass / ((4.0 / 3.0) * M_PI * stellar_radius *
stellar_radius * stellar_radius);
const double density_value = mass / ((4.0 / 3.0) * M_PI * stellar_radius * stellar_radius * stellar_radius);
mfem::ParGridFunction displacement(fem.displacementFes.get());
displacement = 0.0;
@@ -587,36 +525,30 @@ static void run_monopole_case(const std::string &sweep_name,
zero_vacuum_density(fem, density);
mean_field::analysis::conserve_mass(fem, density, mass);
fem.com = mean_field::analysis::get_com(fem, density);
fem.Q = mean_field::physics::compute_quadrupole_moment_tensor(fem, density,
fem.com);
fem.Q = mean_field::physics::compute_quadrupole_moment_tensor(fem, density, fem.com);
const mean_field::physics::GravitySolution solution =
mean_field::physics::solve_gravity_field(fem, args, density,
displacement);
mean_field::physics::solve_gravity_field(fem, args, density, displacement);
auto analytic_potential = [mass,
stellar_radius](const mfem::Vector &position) {
auto analytic_potential = [mass, stellar_radius](const mfem::Vector &position) {
const double radius = position.Norml2();
if (radius >= stellar_radius) {
return -mean_field::utils::G * mass / radius;
}
return -mean_field::utils::G * mass *
(3.0 * stellar_radius * stellar_radius - radius * radius) /
return -mean_field::utils::G * mass * (3.0 * stellar_radius * stellar_radius - radius * radius) /
(2.0 * stellar_radius * stellar_radius * stellar_radius);
};
mean_field::mapping::PhysicalPositionFunctionCoefficient
potential_coefficient(*fem.domainMapperStateless, *fem.displacement,
*fem.compactificationCoordinate,
analytic_potential);
mean_field::mapping::PhysicalPositionFunctionCoefficient potential_coefficient(
*fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate, analytic_potential
);
mfem::ParGridFunction projected_potential(fem.gravityPotentialFes.get());
projected_potential.ProjectCoefficient(potential_coefficient);
const mfem::Vector projected_gradient =
project_monopole_gradient(fem, displacement, mass, stellar_radius);
const mfem::Vector projected_gradient = project_monopole_gradient(fem, displacement, mass, stellar_radius);
const AccuracyBudgetMetrics metrics = measure_monopole_accuracy(
fem, density, displacement, solution, projected_potential,
projected_gradient, mass, stellar_radius);
fem, density, displacement, solution, projected_potential, projected_gradient, mass, stellar_radius
);
REQUIRE(std::isfinite(metrics.direct_relative_residual));
REQUIRE(std::isfinite(metrics.gradient_relative_error));
@@ -630,45 +562,44 @@ static void run_monopole_case(const std::string &sweep_name,
{"mesh_file", args.mesh_file}},
{{"direct_relative_residual", metrics.direct_relative_residual},
{"gradient_relative_error", metrics.gradient_relative_error},
{"gradient_projection_relative_error",
metrics.gradient_projection_relative_error},
{"gradient_solution_projection_gap",
metrics.gradient_solution_projection_gap},
{"gradient_projection_relative_error", metrics.gradient_projection_relative_error},
{"gradient_solution_projection_gap", metrics.gradient_solution_projection_gap},
{"potential_relative_error", metrics.potential_relative_error},
{"potential_projection_relative_error",
metrics.potential_projection_relative_error},
{"potential_solution_projection_gap",
metrics.potential_solution_projection_gap},
{"potential_projection_relative_error", metrics.potential_projection_relative_error},
{"potential_solution_projection_gap", metrics.potential_solution_projection_gap},
{"binding_relative_error", metrics.binding_relative_error},
{"virial_relative_error", metrics.virial_relative_error},
{"virial_consistency_error", metrics.virial_consistency_error}});
{"virial_consistency_error", metrics.virial_consistency_error}}
);
}
TEST_CASE("Uniform Monopole Accuracy Budget: Solver Tolerance",
tags::gravity_analytic_accuracy) {
TEST_CASE(
"Uniform Monopole Accuracy Budget: Solver Tolerance",
tags::gravity_analytic_accuracy
) {
const mean_field::utils::Args args = test_utils::setup_args();
constexpr std::array<double, 4> solver_tolerances{1.0e-8, 1.0e-10, 1.0e-12,
1.0e-14};
constexpr std::array<double, 4> solver_tolerances{1.0e-8, 1.0e-10, 1.0e-12, 1.0e-14};
for (const double solver_tolerance : solver_tolerances) {
run_monopole_case("solver_tolerance", "uniform_monopole", args,
solver_tolerance, 0);
run_monopole_case("solver_tolerance", "uniform_monopole", args, solver_tolerance, 0);
}
}
TEST_CASE("Uniform Monopole Accuracy Budget: Quadrature",
tags::gravity_analytic_accuracy) {
TEST_CASE(
"Uniform Monopole Accuracy Budget: Quadrature",
tags::gravity_analytic_accuracy
) {
const mean_field::utils::Args args = test_utils::setup_args();
constexpr std::array<int, 3> quadrature_boosts{0, 4, 8};
for (const int quadrature_boost : quadrature_boosts) {
run_monopole_case("quadrature", "uniform_monopole", args, 1.0e-13,
quadrature_boost);
run_monopole_case("quadrature", "uniform_monopole", args, 1.0e-13, quadrature_boost);
}
}
TEST_CASE("Uniform Monopole Accuracy Budget: Projection Decomposition",
tags::gravity_analytic_accuracy) {
run_monopole_case("projection_decomposition", "uniform_monopole",
test_utils::setup_args(), 1.0e-13, 0);
TEST_CASE(
"Uniform Monopole Accuracy Budget: Projection Decomposition",
tags::gravity_analytic_accuracy
) {
run_monopole_case("projection_decomposition", "uniform_monopole", test_utils::setup_args(), 1.0e-13, 0);
}

View File

@@ -45,10 +45,8 @@ namespace {
);
m_stellarOperator.GetGravityOperator().ApplyGravityUnknowns(
gravityGradientDirection,
gravityPotentialDirection,
m_stellarOperator.GetGravityContext().GetGeometryContext(),
gravityAction
gravityGradientDirection, gravityPotentialDirection,
m_stellarOperator.GetGravityContext().GetGeometryContext(), gravityAction
);
}
@@ -113,23 +111,11 @@ namespace {
);
}
void apply_centering_rows(
const mean_field::operators::PreparedStellarEquilibriumOperator &stellarOperator,
const mfem::Vector &direction,
mfem::Vector &action
) {
const auto &layout = stellarOperator.GetLayout();
const mfem::Vector displacementDirection =
experiment::null_space::const_value_view(direction, layout, experiment::null_space::displacementValue);
mfem::Vector displacementAction =
experiment::null_space::residual_view(action, layout, experiment::null_space::displacementResidual);
stellarOperator.GetCenteringConstraintOperator().ApplyJacobianRows(displacementDirection, displacementAction);
}
} // namespace
TEST_CASE(
"Gravity-Completed Rigid Motion Responses Of The Stellar Equilibrium Jacobian",
"[null_space][gravity_completed]"
"Gravity-Completed Reduced Surface Mode Responses Of The Stellar Equilibrium Jacobian",
"[null_space][surface_modes][gravity_completed]"
) {
mean_field::utils::Args args = test_utils::setup_args();
args.p.rtol = 1.0e-11;
@@ -141,7 +127,7 @@ TEST_CASE(
int rank = 0;
MPI_Comm_rank(communicator, &rank);
const auto modes = experiment::null_space::make_rigid_modes(fixture);
const auto modes = experiment::null_space::make_surface_modes(fixture);
constexpr std::array<double, 2> rotationFractions{0.0, 0.5};
const int totalCases = static_cast<int>(rotationFractions.size() * modes.size());
int completedCases = 0;
@@ -163,16 +149,16 @@ TEST_CASE(
gravitySolver.SetMaxIter(2000);
gravitySolver.SetPrintLevel(1);
for (const experiment::null_space::RigidMode &mode : modes) {
for (const experiment::null_space::SurfaceMode &mode : modes) {
experiment::null_space::report_progress(
communicator, "solving the gravity completion for " + mode.name + " at rotation fraction " +
std::to_string(rotationFraction) + " (" + std::to_string(completedCases + 1) + "/" +
std::to_string(totalCases) + ")"
);
const mfem::Vector displacementOnlyAction = fixture.unpinned_jacobian_action(mode.direction);
const mfem::Vector surfaceOnlyAction = fixture.jacobian_action(mode.direction);
mfem::Vector gravityRightHandSide =
gravity_residual_blocks(displacementOnlyAction, fixture.stellar_operator().GetLayout());
gravity_residual_blocks(surfaceOnlyAction, fixture.stellar_operator().GetLayout());
gravityRightHandSide *= -1.0;
mfem::Vector gravityCompletion(gravityUnknownJacobian.Width());
@@ -201,25 +187,14 @@ TEST_CASE(
gravityUnknownJacobian.gravity_gradient_size()
);
const mfem::Vector completedUnpinnedAction = fixture.unpinned_jacobian_action(completedDirection);
mfem::Vector completedConstrainedAction(completedUnpinnedAction);
apply_centering_rows(fixture.stellar_operator(), completedDirection, completedConstrainedAction);
mfem::Vector centeringContribution(completedConstrainedAction);
centeringContribution -= completedUnpinnedAction;
const mfem::Vector completedAction = fixture.jacobian_action(completedDirection);
std::map<std::string, double> metrics{
{"displacement_only_input_norm", experiment::null_space::global_norm(mode.direction, communicator)},
{"surface_only_input_norm", experiment::null_space::global_norm(mode.direction, communicator)},
{"gravity_completion_norm", experiment::null_space::global_norm(gravityCompletion, communicator)},
{"completed_input_norm", experiment::null_space::global_norm(completedDirection, communicator)},
{"displacement_only_action_norm",
experiment::null_space::global_norm(displacementOnlyAction, communicator)},
{"gravity_completed_unpinned_action_norm",
experiment::null_space::global_norm(completedUnpinnedAction, communicator)},
{"gravity_completed_constrained_action_norm",
experiment::null_space::global_norm(completedConstrainedAction, communicator)},
{"centering_contribution_norm",
experiment::null_space::global_norm(centeringContribution, communicator)},
{"surface_only_action_norm", experiment::null_space::global_norm(surfaceOnlyAction, communicator)},
{"gravity_completed_action_norm", experiment::null_space::global_norm(completedAction, communicator)},
{"gravity_solve_rhs_norm", gravityRightHandSideNorm},
{"gravity_solve_residual_norm", gravitySolveResidualNorm},
{"gravity_solve_relative_residual", gravitySolveRelativeResidual},
@@ -228,29 +203,22 @@ TEST_CASE(
};
add_block_metrics(
metrics, "displacement_only_",
metrics, "surface_only_",
experiment::null_space::residual_block_norms(
displacementOnlyAction, fixture.stellar_operator().GetLayout(), communicator
surfaceOnlyAction, fixture.stellar_operator().GetLayout(), communicator
)
);
add_block_metrics(
metrics, "gravity_completed_unpinned_",
metrics, "gravity_completed_",
experiment::null_space::residual_block_norms(
completedUnpinnedAction, fixture.stellar_operator().GetLayout(), communicator
)
);
add_block_metrics(
metrics, "gravity_completed_constrained_",
experiment::null_space::residual_block_norms(
completedConstrainedAction, fixture.stellar_operator().GetLayout(), communicator
completedAction, fixture.stellar_operator().GetLayout(), communicator
)
);
if (rank == 0) {
experiment::record_experiment_result(
"gravity_completed_stellar_rigid_motion_null_space", mode.name,
{{"mode_kind",
mode.kind == experiment::null_space::RigidModeKind::translation ? "translation" : "rotation"},
"gravity_completed_reduced_surface_modes", mode.name,
{{"mode_kind", experiment::null_space::surface_mode_kind_name(mode.kind)},
{"axis", std::to_string(mode.axis)},
{"rotation_fraction_of_keplerian", std::to_string(rotationFraction)},
{"mesh_file", test_utils::setup_args().mesh_file},
@@ -262,12 +230,12 @@ TEST_CASE(
++completedCases;
experiment::null_space::report_progress(
communicator, "completed " + std::to_string(completedCases) + "/" + std::to_string(totalCases) +
" gravity-completed rigid-mode cases"
" gravity-completed reduced surface-mode cases"
);
}
}
experiment::null_space::report_progress(
communicator, "gravity-completed rigid-motion probe complete; writing CSV output"
communicator, "gravity-completed reduced surface-mode probe complete; writing CSV output"
);
}

View File

@@ -5,7 +5,9 @@
#include <cmath>
#include <limits>
#include <map>
#include <stdexcept>
#include <string>
#include <vector>
#include <mfem.hpp>
#include <mpi.h>
@@ -16,6 +18,32 @@ import mean_field;
import test_helpers;
namespace {
struct DeterminantPolynomial final {
double linear{0.0};
double quadratic{0.0};
double cubic{0.0};
};
struct CriticalAmplitude final {
double magnitude{0.0};
double determinant{1.0};
bool searchLimitReached{false};
};
struct SymmetricFiniteDifferenceStep final {
double step{0.0};
double positiveMinimumDeterminant{0.0};
double negativeMinimumDeterminant{0.0};
};
struct PolynomialRoots final {
std::array<double, 3> values{
std::numeric_limits<double>::quiet_NaN(), std::numeric_limits<double>::quiet_NaN(),
std::numeric_limits<double>::quiet_NaN()
};
int count{0};
};
[[nodiscard]] double relative_difference(
const mfem::Vector &computed,
const mfem::Vector &reference,
@@ -43,11 +71,234 @@ namespace {
metrics.emplace(prefix + experiment::null_space::residualBlockNames[block] + "_norm", norms[block]);
}
}
[[nodiscard]] std::vector<DeterminantPolynomial> collect_determinant_polynomials(
const mean_field::fem::FEM &fem,
const mfem::Vector &unitVolumeDirection
) {
MFEM_VERIFY(fem.mesh->SpaceDimension() == 3, "The spherical-harmonic frequency probe requires 3D geometry.");
mfem::ParGridFunction displacement(fem.displacementFes.get());
displacement.SetFromTrueDofs(unitVolumeDirection);
std::vector<DeterminantPolynomial> polynomials;
polynomials.reserve(static_cast<std::size_t>(fem.mesh->GetNE()) * 64);
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 int integrationOrder =
std::max(finiteElement->GetOrder() + 2, 2 * fem.mesh->SpaceDimension() * finiteElement->GetOrder());
const mfem::IntegrationRule &rule = mfem::IntRules.Get(transformation->GetGeometryType(), integrationOrder);
for (int point = 0; point < rule.GetNPoints(); ++point) {
transformation->SetIntPoint(&rule.IntPoint(point));
mfem::DenseMatrix gradient;
displacement.GetVectorGradient(*transformation, gradient);
double trace = 0.0;
double traceSquared = 0.0;
for (int row = 0; row < 3; ++row) {
trace += gradient(row, row);
for (int column = 0; column < 3; ++column) {
traceSquared += gradient(row, column) * gradient(column, row);
}
}
polynomials.push_back(
{.linear = trace, .quadratic = 0.5 * (trace * trace - traceSquared), .cubic = gradient.Det()}
);
}
}
return polynomials;
}
[[nodiscard]] double global_minimum_determinant(
const std::vector<DeterminantPolynomial> &polynomials,
const double amplitude,
const MPI_Comm communicator
) {
double localMinimum = std::numeric_limits<double>::infinity();
for (const DeterminantPolynomial &polynomial : polynomials) {
const double determinant =
1.0 +
amplitude * (polynomial.linear + amplitude * (polynomial.quadratic + amplitude * polynomial.cubic));
localMinimum = std::min(localMinimum, determinant);
}
double globalMinimum = std::numeric_limits<double>::infinity();
MPI_Allreduce(&localMinimum, &globalMinimum, 1, MPI_DOUBLE, MPI_MIN, communicator);
return globalMinimum;
}
[[nodiscard]] double evaluate(
const DeterminantPolynomial &polynomial,
const double amplitude
) {
return 1.0 +
amplitude * (polynomial.linear + amplitude * (polynomial.quadratic + amplitude * polynomial.cubic));
}
void append_root(
PolynomialRoots &roots,
const double root
) {
if (roots.count < static_cast<int>(roots.values.size()) && std::isfinite(root)) {
roots.values[static_cast<std::size_t>(roots.count++)] = root;
}
}
[[nodiscard]] PolynomialRoots real_roots(const DeterminantPolynomial &polynomial) {
PolynomialRoots roots;
const double coefficientScale =
std::max({1.0, std::abs(polynomial.linear), std::abs(polynomial.quadratic), std::abs(polynomial.cubic)});
const double tolerance = 64.0 * std::numeric_limits<double>::epsilon() * coefficientScale;
if (std::abs(polynomial.cubic) <= tolerance) {
if (std::abs(polynomial.quadratic) <= tolerance) {
if (std::abs(polynomial.linear) > tolerance) {
append_root(roots, -1.0 / polynomial.linear);
}
return roots;
}
const double discriminant = polynomial.linear * polynomial.linear - 4.0 * polynomial.quadratic;
const double discriminantTolerance =
64.0 * std::numeric_limits<double>::epsilon() * std::max(1.0, polynomial.linear * polynomial.linear);
if (discriminant < -discriminantTolerance) {
return roots;
}
const double squareRoot = std::sqrt(std::max(0.0, discriminant));
const double stableNumerator = -0.5 * (polynomial.linear + std::copysign(squareRoot, polynomial.linear));
if (stableNumerator == 0.0) {
append_root(roots, -polynomial.linear / (2.0 * polynomial.quadratic));
} else {
append_root(roots, stableNumerator / polynomial.quadratic);
if (squareRoot > std::sqrt(discriminantTolerance)) {
append_root(roots, 1.0 / stableNumerator);
}
}
return roots;
}
const double quadratic = polynomial.quadratic / polynomial.cubic;
const double linear = polynomial.linear / polynomial.cubic;
const double constant = 1.0 / polynomial.cubic;
const double depressedLinear = linear - quadratic * quadratic / 3.0;
const double depressedConstant =
2.0 * quadratic * quadratic * quadratic / 27.0 - quadratic * linear / 3.0 + constant;
const double halfConstant = 0.5 * depressedConstant;
const double thirdLinear = depressedLinear / 3.0;
const double discriminant = halfConstant * halfConstant + thirdLinear * thirdLinear * thirdLinear;
const double discriminantTolerance =
128.0 * std::numeric_limits<double>::epsilon() *
std::max({1.0, std::abs(halfConstant * halfConstant), std::abs(thirdLinear * thirdLinear * thirdLinear)});
const double shift = quadratic / 3.0;
if (discriminant > discriminantTolerance) {
const double squareRoot = std::sqrt(discriminant);
append_root(roots, std::cbrt(-halfConstant + squareRoot) + std::cbrt(-halfConstant - squareRoot) - shift);
} else if (std::abs(depressedLinear) <= tolerance || thirdLinear >= 0.0) {
append_root(roots, std::cbrt(-depressedConstant) - shift);
} else {
const double radius = 2.0 * std::sqrt(std::max(0.0, -thirdLinear));
const double cosineArgument = std::clamp(
-halfConstant / std::sqrt(std::max(0.0, -thirdLinear * thirdLinear * thirdLinear)), -1.0, 1.0
);
const double phase = std::acos(cosineArgument) / 3.0;
constexpr double twoPiOverThree = 2.0943951023931954923;
for (int root = 0; root < 3; ++root) {
append_root(roots, radius * std::cos(phase - twoPiOverThree * static_cast<double>(root)) - shift);
}
}
for (int root = 0; root < roots.count; ++root) {
double &value = roots.values[static_cast<std::size_t>(root)];
for (int iteration = 0; iteration < 3; ++iteration) {
const double derivative =
polynomial.linear + value * (2.0 * polynomial.quadratic + 3.0 * value * polynomial.cubic);
if (std::abs(derivative) <= tolerance) {
break;
}
value -= evaluate(polynomial, value) / derivative;
}
}
return roots;
}
[[nodiscard]] CriticalAmplitude find_critical_amplitude(
const std::vector<DeterminantPolynomial> &polynomials,
const double sign,
const MPI_Comm communicator
) {
constexpr double maximumSearchMagnitude = 0.5;
MFEM_VERIFY(sign == 1.0 || sign == -1.0, "The critical-amplitude direction must be positive or negative.");
double localCriticalMagnitude = std::numeric_limits<double>::infinity();
for (const DeterminantPolynomial &polynomial : polynomials) {
const PolynomialRoots roots = real_roots(polynomial);
for (int root = 0; root < roots.count; ++root) {
const double signedMagnitude = sign * roots.values[static_cast<std::size_t>(root)];
if (signedMagnitude > 0.0) {
localCriticalMagnitude = std::min(localCriticalMagnitude, signedMagnitude);
}
}
}
double globalCriticalMagnitude = std::numeric_limits<double>::infinity();
MPI_Allreduce(&localCriticalMagnitude, &globalCriticalMagnitude, 1, MPI_DOUBLE, MPI_MIN, communicator);
if (!std::isfinite(globalCriticalMagnitude) || globalCriticalMagnitude > maximumSearchMagnitude) {
return {
.magnitude = maximumSearchMagnitude,
.determinant = global_minimum_determinant(polynomials, sign * maximumSearchMagnitude, communicator),
.searchLimitReached = true
};
}
return {
.magnitude = globalCriticalMagnitude,
.determinant = global_minimum_determinant(polynomials, sign * globalCriticalMagnitude, communicator),
.searchLimitReached = false
};
}
[[nodiscard]] SymmetricFiniteDifferenceStep find_symmetric_finite_difference_step(
const mean_field::deformation::PreparedDomainDeformationRuntime &deformation,
const mfem::Vector &unitVolumeDirection
) {
constexpr double requestedStep = 1.0e-4;
constexpr double minimumStep = 1.0e-10;
mfem::Vector trialVolumeDirection(unitVolumeDirection.Size());
for (double step = requestedStep; step >= minimumStep; step *= 0.25) {
trialVolumeDirection = unitVolumeDirection;
trialVolumeDirection *= step;
const mean_field::deformation::DomainDeformationGeometryReport positive =
deformation.inspectMappedGeometry(trialVolumeDirection);
trialVolumeDirection *= -1.0;
const mean_field::deformation::DomainDeformationGeometryReport negative =
deformation.inspectMappedGeometry(trialVolumeDirection);
if (positive.isOrientationPreserving() && negative.isOrientationPreserving()) {
return {
.step = step,
.positiveMinimumDeterminant = positive.minimumJacobianDeterminant,
.negativeMinimumDeterminant = negative.minimumJacobianDeterminant
};
}
}
throw std::domain_error(
"No symmetric orientation-preserving finite-difference step was found for the surface mode."
);
}
} // namespace
TEST_CASE(
"Rigid Motion Responses Of The Stellar Equilibrium Jacobian",
"[null_space][rigid_motion]"
"Reduced Surface Mode Reachability And Stellar Equilibrium Linearization",
"[null_space][surface_modes][reachability][linearization]"
) {
mean_field::utils::Args args = test_utils::setup_args();
args.p.rtol = 1.0e-12;
@@ -59,9 +310,8 @@ TEST_CASE(
int rank = 0;
MPI_Comm_rank(communicator, &rank);
const auto modes = experiment::null_space::make_rigid_modes(fixture);
const auto modes = experiment::null_space::make_surface_modes(fixture);
constexpr std::array<double, 2> rotationFractions{0.0, 0.5};
constexpr std::array<double, 2> finiteDifferenceSteps{1.0e-4, 1.0e-6};
const int totalCases = static_cast<int>(rotationFractions.size() * modes.size());
int completedCases = 0;
@@ -69,59 +319,53 @@ TEST_CASE(
const mean_field::physics::RigidRotation rotation = fixture.rotation(rotationFraction);
fixture.prepare(fixture.state(), rotation);
mfem::Vector constrainedResidual;
fixture.stellar_operator().BuildResidual(constrainedResidual);
const mfem::Vector unpinnedResidual = fixture.unpinned_residual();
const mfem::Vector baseResidual = fixture.residual();
REQUIRE(std::isfinite(experiment::null_space::global_norm(baseResidual, communicator)));
REQUIRE(constrainedResidual.Size() == unpinnedResidual.Size());
REQUIRE(std::isfinite(experiment::null_space::global_norm(constrainedResidual, communicator)));
REQUIRE(std::isfinite(experiment::null_space::global_norm(unpinnedResidual, communicator)));
for (const experiment::null_space::RigidMode &mode : modes) {
for (const experiment::null_space::SurfaceMode &mode : modes) {
experiment::null_space::report_progress(
communicator, "probing " + mode.name + " at rotation fraction " + std::to_string(rotationFraction) +
" (" + std::to_string(completedCases + 1) + "/" + std::to_string(totalCases) + ")"
);
fixture.prepare(fixture.state(), rotation);
const mfem::Vector unpinnedAction = fixture.unpinned_jacobian_action(mode.direction);
mfem::Vector constrainedAction;
fixture.stellar_operator().Mult(mode.direction, constrainedAction);
mfem::Vector centeringContribution(constrainedAction);
centeringContribution -= unpinnedAction;
const mfem::Vector action = fixture.jacobian_action(mode.direction);
const mfem::Vector liftedDirection = fixture.lifted_surface_direction(mode.direction);
const double inputNorm = experiment::null_space::global_norm(mode.direction, communicator);
const double unpinnedNorm = experiment::null_space::global_norm(unpinnedAction, communicator);
const double constrainedNorm = experiment::null_space::global_norm(constrainedAction, communicator);
const double actionNorm = experiment::null_space::global_norm(action, communicator);
const double liftNorm = experiment::null_space::global_norm(liftedDirection, communicator);
const SymmetricFiniteDifferenceStep coarseStep = find_symmetric_finite_difference_step(
fixture.stellar_operator().GetDomainDeformation(), liftedDirection
);
const std::array<double, 2> finiteDifferenceSteps{coarseStep.step, 1.0e-2 * coarseStep.step};
REQUIRE(inputNorm > 0.0);
REQUIRE(std::isfinite(unpinnedNorm));
REQUIRE(std::isfinite(constrainedNorm));
REQUIRE(liftNorm > 0.0);
REQUIRE(std::isfinite(actionNorm));
std::map<std::string, double> metrics{
{"input_algebraic_norm", inputNorm},
{"unpinned_action_norm", unpinnedNorm},
{"unpinned_action_per_input_norm", unpinnedNorm / inputNorm},
{"constrained_action_norm", constrainedNorm},
{"constrained_action_per_input_norm", constrainedNorm / inputNorm},
{"centering_contribution_norm",
experiment::null_space::global_norm(centeringContribution, communicator)},
{"unpinned_base_residual_norm", experiment::null_space::global_norm(unpinnedResidual, communicator)},
{"constrained_base_residual_norm",
experiment::null_space::global_norm(constrainedResidual, communicator)}
{"surface_parameter_input_norm", inputNorm},
{"lifted_volume_displacement_norm", liftNorm},
{"lift_amplification", liftNorm / inputNorm},
{"root_jacobian_action_norm", actionNorm},
{"root_action_per_surface_parameter_norm", actionNorm / inputNorm},
{"root_action_per_lifted_volume_norm", actionNorm / liftNorm},
{"base_residual_norm", experiment::null_space::global_norm(baseResidual, communicator)},
{"surface_parameter_count",
static_cast<double>(fixture.stellar_operator().GetDomainDeformation().parameterCount())},
{"volume_displacement_count",
static_cast<double>(fixture.stellar_operator().GetDomainDeformation().volumeDisplacementSize())},
{"finite_difference_coarse_step", finiteDifferenceSteps[0]},
{"finite_difference_fine_step", finiteDifferenceSteps[1]},
{"coarse_step_positive_minimum_determinant", coarseStep.positiveMinimumDeterminant},
{"coarse_step_negative_minimum_determinant", coarseStep.negativeMinimumDeterminant}
};
add_block_metrics(
metrics, "unpinned_",
metrics, "root_",
experiment::null_space::residual_block_norms(
unpinnedAction, fixture.stellar_operator().GetLayout(), communicator
)
);
add_block_metrics(
metrics, "constrained_",
experiment::null_space::residual_block_norms(
constrainedAction, fixture.stellar_operator().GetLayout(), communicator
action, fixture.stellar_operator().GetLayout(), communicator
)
);
@@ -129,21 +373,21 @@ TEST_CASE(
mfem::Vector plusState(fixture.state());
plusState.Add(step, mode.direction);
fixture.prepare(plusState, rotation);
const mfem::Vector plusResidual = fixture.unpinned_residual();
const mfem::Vector plusResidual = fixture.residual();
mfem::Vector minusState(fixture.state());
minusState.Add(-step, mode.direction);
fixture.prepare(minusState, rotation);
const mfem::Vector minusResidual = fixture.unpinned_residual();
const mfem::Vector minusResidual = fixture.residual();
mfem::Vector finiteDifference(plusResidual);
finiteDifference -= minusResidual;
finiteDifference /= 2.0 * step;
const std::string stepName = step == finiteDifferenceSteps.front() ? "1e-4" : "1e-6";
const std::string stepName = step == finiteDifferenceSteps.front() ? "coarse" : "fine";
metrics.emplace(
"finite_difference_relative_error_" + stepName,
relative_difference(unpinnedAction, finiteDifference, communicator)
relative_difference(action, finiteDifference, communicator)
);
}
@@ -151,9 +395,8 @@ TEST_CASE(
if (rank == 0) {
experiment::record_experiment_result(
"stellar_rigid_motion_null_space", mode.name,
{{"mode_kind",
mode.kind == experiment::null_space::RigidModeKind::translation ? "translation" : "rotation"},
"reduced_surface_mode_reachability", mode.name,
{{"mode_kind", experiment::null_space::surface_mode_kind_name(mode.kind)},
{"axis", std::to_string(mode.axis)},
{"rotation_fraction_of_keplerian", std::to_string(rotationFraction)},
{"mesh_file", test_utils::setup_args().mesh_file},
@@ -164,11 +407,123 @@ TEST_CASE(
++completedCases;
experiment::null_space::report_progress(
communicator,
"completed " + std::to_string(completedCases) + "/" + std::to_string(totalCases) + " rigid-mode cases"
communicator, "completed " + std::to_string(completedCases) + "/" + std::to_string(totalCases) +
" reduced surface-mode cases"
);
}
}
experiment::null_space::report_progress(communicator, "rigid-motion probe complete; writing CSV output");
experiment::null_space::report_progress(communicator, "reduced surface-mode probe complete; writing CSV output");
}
TEST_CASE(
"Spherical Harmonic Surface Frequencies Preserve Orientation Up To Measured Critical Amplitudes",
"[surface_modes][frequency_limit][geometry][spherical_harmonic]"
) {
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());
experiment::null_space::Model model = experiment::null_space::make_model();
auto deformation = model.compileDomainDeformation(fem);
const auto &surface = deformation.surfaceDeformationPrescription();
const MPI_Comm communicator = fem.mesh->GetComm();
int rank = 0;
MPI_Comm_rank(communicator, &rank);
constexpr std::array<int, 13> angularDegrees{0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 14, 16, 20};
mfem::Vector zeroParameters(surface.parameterCount());
zeroParameters = 0.0;
for (std::size_t degreeIndex = 0; degreeIndex < angularDegrees.size(); ++degreeIndex) {
const int angularDegree = angularDegrees[degreeIndex];
experiment::null_space::report_progress(
communicator, "measuring zonal spherical-harmonic degree " + std::to_string(angularDegree) + " (" +
std::to_string(degreeIndex + 1) + "/" + std::to_string(angularDegrees.size()) + ")"
);
mfem::Vector parameters(surface.parameterCount());
double localMaximumAngularMagnitude = 0.0;
for (int parameter = 0; parameter < parameters.Size(); ++parameter) {
const double angularValue =
experiment::null_space::zonal_legendre(angularDegree, surface.radialDirection(parameter, 2));
parameters(parameter) = surface.referenceRadius(parameter) * angularValue;
localMaximumAngularMagnitude = std::max(localMaximumAngularMagnitude, std::abs(angularValue));
}
double globalMaximumAngularMagnitude = 0.0;
MPI_Allreduce(
&localMaximumAngularMagnitude, &globalMaximumAngularMagnitude, 1, MPI_DOUBLE, MPI_MAX, communicator
);
REQUIRE(globalMaximumAngularMagnitude > 0.0);
parameters /= globalMaximumAngularMagnitude;
mfem::Vector unitVolumeDirection(deformation.volumeDisplacementSize());
deformation.applyJacobian(zeroParameters, parameters, unitVolumeDirection);
const std::vector<DeterminantPolynomial> determinantPolynomials =
collect_determinant_polynomials(fem, unitVolumeDirection);
long long localSampleCount = static_cast<long long>(determinantPolynomials.size());
long long globalSampleCount = 0;
MPI_Allreduce(&localSampleCount, &globalSampleCount, 1, MPI_LONG_LONG, MPI_SUM, communicator);
REQUIRE(globalSampleCount > 0);
const CriticalAmplitude positiveCritical = find_critical_amplitude(determinantPolynomials, 1.0, communicator);
const CriticalAmplitude negativeCritical = find_critical_amplitude(determinantPolynomials, -1.0, communicator);
const double determinantPositive1e4 = global_minimum_determinant(determinantPolynomials, 1.0e-4, communicator);
const double determinantNegative1e4 = global_minimum_determinant(determinantPolynomials, -1.0e-4, communicator);
const double determinantPositive1e3 = global_minimum_determinant(determinantPolynomials, 1.0e-3, communicator);
const double determinantNegative1e3 = global_minimum_determinant(determinantPolynomials, -1.0e-3, communicator);
const double determinantPositive1e2 = global_minimum_determinant(determinantPolynomials, 1.0e-2, communicator);
const double determinantNegative1e2 = global_minimum_determinant(determinantPolynomials, -1.0e-2, communicator);
if (angularDegree == 12) {
mfem::Vector directInspectionDirection(unitVolumeDirection);
directInspectionDirection *= 1.0e-3;
const mean_field::deformation::DomainDeformationGeometryReport directInspection =
deformation.inspectMappedGeometry(directInspectionDirection);
const double comparisonScale = std::max(
{1.0, std::abs(directInspection.minimumJacobianDeterminant), std::abs(determinantPositive1e3)}
);
CHECK(
std::abs(directInspection.minimumJacobianDeterminant - determinantPositive1e3) <=
1.0e-11 * comparisonScale
);
}
REQUIRE(std::isfinite(positiveCritical.magnitude));
REQUIRE(std::isfinite(negativeCritical.magnitude));
REQUIRE(positiveCritical.magnitude > 0.0);
REQUIRE(negativeCritical.magnitude > 0.0);
if (rank == 0) {
experiment::record_experiment_result(
"spherical_harmonic_surface_frequency_limit", "zonal_l" + std::to_string(angularDegree),
{{"angular_degree", std::to_string(angularDegree)},
{"azimuthal_order", "0"},
{"positive_limit_censored", positiveCritical.searchLimitReached ? "true" : "false"},
{"negative_limit_censored", negativeCritical.searchLimitReached ? "true" : "false"},
{"mesh_file", test_utils::setup_args().mesh_file}},
{{"positive_critical_fractional_amplitude", positiveCritical.magnitude},
{"negative_critical_fractional_amplitude", negativeCritical.magnitude},
{"positive_critical_determinant", positiveCritical.determinant},
{"negative_critical_determinant", negativeCritical.determinant},
{"minimum_determinant_positive_1e-4", determinantPositive1e4},
{"minimum_determinant_negative_1e-4", determinantNegative1e4},
{"minimum_determinant_positive_1e-3", determinantPositive1e3},
{"minimum_determinant_negative_1e-3", determinantNegative1e3},
{"minimum_determinant_positive_1e-2", determinantPositive1e2},
{"minimum_determinant_negative_1e-2", determinantNegative1e2},
{"surface_parameter_norm", experiment::null_space::global_norm(parameters, communicator)},
{"lifted_volume_displacement_norm",
experiment::null_space::global_norm(unitVolumeDirection, communicator)},
{"global_geometry_sample_count", static_cast<double>(globalSampleCount)}}
);
}
}
experiment::null_space::report_progress(
communicator, "spherical-harmonic frequency-limit probe complete; writing CSV output"
);
}

View File

@@ -8,6 +8,7 @@ module;
#include <limits>
#include <string>
#include <utility>
#include <vector>
#include <mfem.hpp>
#include <mpi.h>
@@ -18,14 +19,15 @@ import mean_field;
import test_helpers;
export namespace experiment::null_space {
using Form = mean_field::utils::blocks::barotropic_equilibrium_form;
using Form = mean_field::utils::blocks::surface_deformed_stellar_equilibrium_form;
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
using Model = mean_field::models::StellarModel<mean_field::models::structure::PolytropicStructure>;
constexpr auto densityValue =
mean_field::utils::blocks::get_value_block<Form>(mean_field::utils::blocks::density_field.mass_term);
constexpr auto displacementValue =
mean_field::utils::blocks::get_value_block<Form>(mean_field::utils::blocks::displacement_field.geometry_term);
constexpr auto surfaceDeformationValue = mean_field::utils::blocks::get_value_block<Form>(
mean_field::utils::blocks::surface_deformation_field.parameters_term
);
constexpr auto gravityGradientValue =
mean_field::utils::blocks::get_value_block<Form>(mean_field::utils::blocks::gravity_field.gradient_term);
constexpr auto gravityPotentialValue =
@@ -42,8 +44,8 @@ export namespace experiment::null_space {
mean_field::utils::blocks::get_residual_block<Form>(mean_field::utils::blocks::gravity_field.poisson_term);
constexpr auto densityResidual =
mean_field::utils::blocks::get_residual_block<Form>(mean_field::utils::blocks::density_field.mass_term);
constexpr auto displacementResidual = mean_field::utils::blocks::get_residual_block<Form>(
mean_field::utils::blocks::displacement_field.geometry_term
constexpr auto surfaceShapeResidual = mean_field::utils::blocks::get_residual_block<Form>(
mean_field::utils::blocks::surface_deformation_field.shape_equilibrium_term
);
constexpr auto enthalpyResidual =
mean_field::utils::blocks::get_residual_block<Form>(mean_field::utils::blocks::enthalpy_field.specific_term);
@@ -52,7 +54,7 @@ export namespace experiment::null_space {
);
inline constexpr std::array<const char *, 6> residualBlockNames{"gravity_gradient", "gravity_potential", "closure",
"displacement", "hydrostatic", "mass"};
"surface_shape", "hydrostatic", "mass"};
template <int index>
[[nodiscard]] mfem::Vector value_view(
@@ -97,7 +99,9 @@ export namespace experiment::null_space {
const mean_field::utils::blocks::value_block<index> block,
const mfem::Vector &source
) {
MFEM_VERIFY(source.Size() == layout.size(block), "Null-space experiment received a block with the wrong size.");
MFEM_VERIFY(
source.Size() == layout.size(block), "Surface-mode experiment received a block with the wrong size."
);
value_view(vector, layout, block) = source;
}
@@ -118,7 +122,7 @@ export namespace experiment::null_space {
int rank = 0;
MPI_Comm_rank(communicator, &rank);
if (rank == 0) {
std::cout << "[null-space experiment] " << message << std::endl;
std::cout << "[reduced-surface experiment] " << message << std::endl;
}
}
@@ -126,7 +130,7 @@ export namespace experiment::null_space {
return {
.discretization = {.identity = 2003, .revision = 1},
.density = {.identity = 2011, .revision = 1},
.displacement = {.identity = 2017, .revision = 1},
.surfaceDeformation = {.identity = 2017, .revision = 1},
.gravityGradient = {.identity = 2027, .revision = 1},
.gravityPotential = {.identity = 2029, .revision = 1},
.enthalpy = {.identity = 2039, .revision = 1},
@@ -138,7 +142,7 @@ export namespace experiment::null_space {
inline void increment_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;
@@ -211,6 +215,10 @@ export namespace experiment::null_space {
return m_fem;
}
[[nodiscard]] Model &model() noexcept {
return m_model;
}
[[nodiscard]] mean_field::operators::PreparedStellarEquilibriumOperator &stellar_operator() noexcept {
return m_operator;
}
@@ -248,66 +256,26 @@ export namespace experiment::null_space {
m_operator.Prepare(state, m_dependencies, rotation);
}
[[nodiscard]] mfem::Vector unpinned_residual() const {
const auto &layout = m_operator.GetLayout();
const mfem::Vector reducedDensity = const_value_view(m_currentState, layout, densityValue);
const mfem::Vector displacement = const_value_view(m_currentState, layout, displacementValue);
const mfem::Vector gravityGradient = const_value_view(m_currentState, layout, gravityGradientValue);
const mfem::Vector gravityPotential = const_value_view(m_currentState, layout, gravityPotentialValue);
const mfem::Vector gravityState =
pack_gravity_state(reducedDensity, displacement, gravityGradient, gravityPotential);
mfem::Vector gravity;
mfem::Vector closure;
mfem::Vector displacementRows;
mfem::Vector hydrostatic;
mfem::Vector mass;
m_operator.GetGravityOperator().Mult(gravityState, gravity);
m_operator.GetBarotropicClosureOperator().BuildResidual(closure);
m_operator.GetDisplacementOperator().BuildResidual(displacementRows);
m_operator.GetHydrostaticOperator().BuildResidual(hydrostatic);
m_operator.GetSurfaceConstraintOperator().ApplyResidualRows(hydrostatic);
m_operator.GetMassNormalizationOperator().BuildResidual(mass);
return pack_residual(gravity, closure, displacementRows, hydrostatic, mass);
[[nodiscard]] mfem::Vector residual() const {
mfem::Vector result;
m_operator.BuildResidual(result);
return result;
}
[[nodiscard]] mfem::Vector unpinned_jacobian_action(const mfem::Vector &direction) const {
[[nodiscard]] mfem::Vector jacobian_action(const mfem::Vector &direction) const {
mfem::Vector result;
m_operator.Mult(direction, result);
return result;
}
[[nodiscard]] mfem::Vector lifted_surface_direction(const mfem::Vector &rootDirection) const {
const auto &layout = m_operator.GetLayout();
const mfem::Vector reducedDensityDirection = const_value_view(direction, layout, densityValue);
const mfem::Vector displacementDirection = const_value_view(direction, layout, displacementValue);
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);
const mfem::Vector bernoulliDirection = const_value_view(direction, layout, bernoulliValue);
const mfem::Vector gravityDirection = pack_gravity_state(
reducedDensityDirection, displacementDirection, gravityGradientDirection, gravityPotentialDirection
const mfem::Vector surfaceDirection = const_value_view(rootDirection, layout, surfaceDeformationValue);
mfem::Vector volumeDirection(m_operator.GetDomainDeformation().volumeDisplacementSize());
m_operator.GetDomainDeformation().applyJacobian(
m_operator.GetSurfaceDeformationParameters(), surfaceDirection, volumeDirection
);
mfem::Vector gravity;
mfem::Vector closure;
mfem::Vector displacementRows;
mfem::Vector hydrostatic;
mfem::Vector mass;
m_operator.GetGravityJacobianOperator().Mult(gravityDirection, gravity);
m_operator.GetBarotropicClosureOperator().Mult(
reducedDensityDirection, reducedEnthalpyDirection, displacementDirection, closure
);
m_operator.GetDisplacementOperator().ApplyCompleteJacobianAction(
reducedDensityDirection, displacementDirection, gravityGradientDirection, reducedEnthalpyDirection,
displacementRows
);
m_operator.GetHydrostaticOperator().ApplyCompleteJacobianAction(
reducedEnthalpyDirection, gravityPotentialDirection, bernoulliDirection(0), displacementDirection,
hydrostatic
);
m_operator.GetSurfaceConstraintOperator().ApplyJacobianRows(reducedEnthalpyDirection, hydrostatic);
m_operator.GetMassNormalizationOperator().ApplyCompleteJacobianAction(
reducedDensityDirection, displacementDirection, mass
);
return pack_residual(gravity, closure, displacementRows, hydrostatic, mass);
return volumeDirection;
}
private:
@@ -375,12 +343,10 @@ export namespace experiment::null_space {
mfem::Vector densityTrue;
mfem::Vector enthalpyTrue;
mfem::Vector displacementTrue;
mfem::Vector gravityGradientTrue;
mfem::Vector gravityPotentialTrue;
densityField.GetTrueDofs(densityTrue);
enthalpyField.GetTrueDofs(enthalpyTrue);
displacementField.GetTrueDofs(displacementTrue);
gravity.gradPhi.GetTrueDofs(gravityGradientTrue);
gravity.phi.GetTrueDofs(gravityPotentialTrue);
@@ -390,8 +356,10 @@ export namespace experiment::null_space {
const mean_field::field::FieldDofMap enthalpyMap =
mean_field::field::make_field_dof_map<mean_field::field::Enthalpy, DomainSchema>(*m_fem.enthalpyFes);
mfem::Vector surfaceParameters(layout.size(surfaceDeformationValue));
surfaceParameters = 0.0;
assign_value_block(m_state, layout, densityValue, densityMap.gather(densityTrue));
assign_value_block(m_state, layout, displacementValue, displacementTrue);
assign_value_block(m_state, layout, surfaceDeformationValue, surfaceParameters);
assign_value_block(m_state, layout, gravityGradientValue, gravityGradientTrue);
assign_value_block(m_state, layout, gravityPotentialValue, gravityPotentialTrue);
assign_value_block(m_state, layout, enthalpyValue, enthalpyMap.gather(enthalpyTrue));
@@ -402,29 +370,6 @@ export namespace experiment::null_space {
report_progress(m_fem.mesh->GetComm(), "analytic state is prepared");
}
[[nodiscard]] mfem::Vector pack_residual(
const mfem::Vector &gravity,
const mfem::Vector &closure,
const mfem::Vector &displacementRows,
const mfem::Vector &hydrostatic,
const mfem::Vector &mass
) const {
const auto &layout = m_operator.GetLayout();
mfem::Vector result(layout.residual_offsets().Last());
result = 0.0;
const mfem::Vector gravityGradient(gravity.GetData(), layout.size(gravityGradientResidual));
const mfem::Vector gravityPotential(
gravity.GetData() + layout.size(gravityGradientResidual), layout.size(gravityPotentialResidual)
);
residual_view(result, layout, gravityGradientResidual) = gravityGradient;
residual_view(result, layout, gravityPotentialResidual) = gravityPotential;
residual_view(result, layout, densityResidual) = closure;
residual_view(result, layout, displacementResidual) = displacementRows;
residual_view(result, layout, enthalpyResidual) = hydrostatic;
residual_view(result, layout, massResidual) = mass;
return result;
}
mean_field::utils::Args m_args;
mean_field::fem::FEM m_fem;
Model m_model;
@@ -434,68 +379,135 @@ export namespace experiment::null_space {
mean_field::operators::StellarEquilibriumDependencies m_dependencies;
};
enum class RigidModeKind : std::uint8_t { translation, rotation };
enum class SurfaceModeKind : std::uint8_t {
uniform_radial,
translation_like_dipole,
oblate_quadrupole,
spherical_harmonic
};
struct RigidMode final {
struct SurfaceMode final {
std::string name;
RigidModeKind kind;
SurfaceModeKind kind;
int axis;
mfem::Vector direction;
};
[[nodiscard]] inline std::array<
RigidMode,
6>
make_rigid_modes(const N3Equilibrium &fixture) {
const auto &fem = fixture.fem();
[[nodiscard]] inline const char *surface_mode_kind_name(const SurfaceModeKind kind) noexcept {
switch (kind) {
case SurfaceModeKind::uniform_radial:
return "uniform_radial";
case SurfaceModeKind::translation_like_dipole:
return "translation_like_dipole";
case SurfaceModeKind::oblate_quadrupole:
return "oblate_quadrupole";
case SurfaceModeKind::spherical_harmonic:
return "spherical_harmonic";
}
return "unknown";
}
[[nodiscard]] inline double zonal_legendre(
const int degree,
const double cosineOfPolarAngle
) {
MFEM_VERIFY(degree >= 0, "A zonal spherical-harmonic degree must be non-negative.");
const double coordinate = std::clamp(cosineOfPolarAngle, -1.0, 1.0);
if (degree == 0) {
return 1.0;
}
if (degree == 1) {
return coordinate;
}
double previousPrevious = 1.0;
double previous = coordinate;
for (int order = 2; order <= degree; ++order) {
const double current = ((2.0 * static_cast<double>(order) - 1.0) * coordinate * previous -
(static_cast<double>(order) - 1.0) * previousPrevious) /
static_cast<double>(order);
previousPrevious = previous;
previous = current;
}
return previous;
}
[[nodiscard]] inline std::vector<SurfaceMode> make_surface_modes(N3Equilibrium &fixture) {
const auto &layout = fixture.stellar_operator().GetLayout();
std::array<RigidMode, 6> modes;
for (int axis = 0; axis < 3; ++axis) {
mfem::ParGridFunction translation(fem.displacementFes.get());
mfem::Vector translationValue(3);
translationValue = 0.0;
translationValue(axis) = 1.0;
mfem::VectorConstantCoefficient coefficient(translationValue);
translation.ProjectCoefficient(coefficient);
mfem::Vector translationTrue;
translation.GetTrueDofs(translationTrue);
auto deformation = fixture.model().compileDomainDeformation(fixture.fem());
const auto &surface = deformation.surfaceDeformationPrescription();
MFEM_VERIFY(
surface.parameterCount() == layout.size(surfaceDeformationValue),
"The diagnostic surface prescription does not match the root surface block."
);
const auto make_root_direction = [&layout](const mfem::Vector &surfaceDirection) {
mfem::Vector direction(layout.value_offsets().Last());
direction = 0.0;
assign_value_block(direction, layout, displacementValue, translationTrue);
modes[axis] = RigidMode{
.name = std::string("translation_") + static_cast<char>('x' + axis),
.kind = RigidModeKind::translation,
.axis = axis,
.direction = std::move(direction)
assign_value_block(direction, layout, surfaceDeformationValue, surfaceDirection);
return direction;
};
std::vector<SurfaceMode> modes;
modes.reserve(6);
mfem::Vector uniform(surface.parameterCount());
for (int parameter = 0; parameter < uniform.Size(); ++parameter) {
uniform(parameter) = surface.referenceRadius(parameter);
}
modes.push_back(
{.name = "uniform_radial_homology",
.kind = SurfaceModeKind::uniform_radial,
.axis = -1,
.direction = make_root_direction(uniform)}
);
for (int axis = 0; axis < surface.spatialDimension(); ++axis) {
mfem::Vector dipole(surface.parameterCount());
for (int parameter = 0; parameter < dipole.Size(); ++parameter) {
dipole(parameter) = surface.radialDirection(parameter, axis);
}
modes.push_back(
{.name = std::string("translation_like_dipole_") + static_cast<char>('x' + axis),
.kind = SurfaceModeKind::translation_like_dipole,
.axis = axis,
.direction = make_root_direction(dipole)}
);
}
for (int axis = 0; axis < 3; ++axis) {
mfem::ParGridFunction rotation(fem.displacementFes.get());
mfem::VectorFunctionCoefficient coefficient(3, [axis](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(3);
value = 0.0;
const int first = (axis + 1) % 3;
const int second = (axis + 2) % 3;
value(first) = -position(second);
value(second) = position(first);
});
rotation.ProjectCoefficient(coefficient);
mfem::Vector rotationTrue;
rotation.GetTrueDofs(rotationTrue);
mfem::Vector direction(layout.value_offsets().Last());
direction = 0.0;
assign_value_block(direction, layout, displacementValue, rotationTrue);
modes[3 + axis] = RigidMode{
.name = std::string("rotation_") + static_cast<char>('x' + axis),
.kind = RigidModeKind::rotation,
.axis = axis,
.direction = std::move(direction)
};
mfem::Vector quadrupole(surface.parameterCount());
for (int parameter = 0; parameter < quadrupole.Size(); ++parameter) {
const double polarDirection = surface.radialDirection(parameter, 2);
quadrupole(parameter) = surface.referenceRadius(parameter) * (1.0 - 3.0 * polarDirection * polarDirection);
}
modes.push_back(
{.name = "axisymmetric_oblate_quadrupole_z",
.kind = SurfaceModeKind::oblate_quadrupole,
.axis = 2,
.direction = make_root_direction(quadrupole)}
);
constexpr int diagnosticAngularDegree = 12;
mfem::Vector sphericalHarmonic(surface.parameterCount());
double localMaximumMagnitude = 0.0;
for (int parameter = 0; parameter < sphericalHarmonic.Size(); ++parameter) {
const double angularValue = zonal_legendre(diagnosticAngularDegree, surface.radialDirection(parameter, 2));
sphericalHarmonic(parameter) = surface.referenceRadius(parameter) * angularValue;
localMaximumMagnitude = std::max(localMaximumMagnitude, std::abs(angularValue));
}
double globalMaximumMagnitude = 0.0;
MPI_Allreduce(
&localMaximumMagnitude, &globalMaximumMagnitude, 1, MPI_DOUBLE, MPI_MAX, fixture.fem().mesh->GetComm()
);
MFEM_VERIFY(globalMaximumMagnitude > 0.0, "The spherical-harmonic surface mode has zero amplitude.");
sphericalHarmonic /= globalMaximumMagnitude;
modes.push_back(
{.name = "zonal_spherical_harmonic_l12",
.kind = SurfaceModeKind::spherical_harmonic,
.axis = -1,
.direction = make_root_direction(sphericalHarmonic)}
);
return modes;
}
@@ -511,7 +523,7 @@ export namespace experiment::null_space {
global_norm(const_residual_view(action, layout, gravityGradientResidual), communicator),
global_norm(const_residual_view(action, layout, gravityPotentialResidual), communicator),
global_norm(const_residual_view(action, layout, densityResidual), communicator),
global_norm(const_residual_view(action, layout, displacementResidual), communicator),
global_norm(const_residual_view(action, layout, surfaceShapeResidual), communicator),
global_norm(const_residual_view(action, layout, enthalpyResidual), communicator),
global_norm(const_residual_view(action, layout, massResidual), communicator)
};

4
format
View File

@@ -1,3 +1,3 @@
#!/bin/bash
find libmeanfield tests -type f \( -name '*.cpp' \) | xargs -I{} clang-format -style=file:clang-format-styles/style -i {}
find libmeanfield tests -type f \( -name '*.cppm' \) | xargs -I{} clang-format -style=file:clang-format-styles/style -i {}
find libmeanfield tests experiments -type f \( -name '*.cpp' \) | xargs -I{} clang-format -style=file:clang-format-styles/style -i {}
find libmeanfield tests experiments -type f \( -name '*.cppm' \) | xargs -I{} clang-format -style=file:clang-format-styles/style -i {}

View File

@@ -0,0 +1,345 @@
module;
#include <cmath>
#include <format>
#include <stdexcept>
#include <utility>
#include <mfem.hpp>
module mean_field;
import :deformation.nodal_radial_surface;
namespace mean_field::deformation {
namespace {
[[nodiscard]] SurfaceDeformationDescriptor nodalRadialDescriptor(const int spatialDimension) noexcept {
return {
.name = "NodalRadialSurface",
.spatialDimension = spatialDimension,
.motionKind = SurfaceMotionKind::Radial,
.linearOnReferenceGeometry = true,
.requiresStarShapedReferenceSurface = true,
.hasExactDerivativeTranspose = true,
.hasExactPullbackDerivative = true,
.translationTreatment = GeometricGaugeTreatment::Retained,
.orientationTreatment = GeometricGaugeTreatment::Retained
};
}
void requireFiniteVector(
const mfem::Vector &vector,
const char *message
) {
for (int index = 0; index < vector.Size(); ++index) {
if (!std::isfinite(vector(index))) {
throw std::invalid_argument(message);
}
}
}
} // namespace
SurfaceDeformationCompilationContext::SurfaceDeformationCompilationContext(
mfem::ParFiniteElementSpace &scalarFiniteElementSpace,
field::ScalarBoundaryDofMap surfaceDofMap
)
: m_scalarFiniteElementSpace(&scalarFiniteElementSpace),
m_surfaceDofMap(std::move(surfaceDofMap)) {
if (scalarFiniteElementSpace.Nonconforming()) {
throw std::invalid_argument(
"Surface deformation compilation currently requires a conforming scalar finite-element space."
);
}
if (scalarFiniteElementSpace.GetVDim() != 1) {
throw std::invalid_argument("Surface deformation compilation requires a scalar finite-element space.");
}
if (scalarFiniteElementSpace.GetMesh() == nullptr) {
throw std::invalid_argument("Surface deformation compilation requires a finite-element mesh.");
}
if (m_surfaceDofMap.volume_true_dof_size() != scalarFiniteElementSpace.GetTrueVSize()) {
throw std::invalid_argument(
"The surface DOF map and scalar finite-element space have incompatible true-DOF sizes."
);
}
if (m_surfaceDofMap.global_size() <= 0) {
throw std::invalid_argument("Surface deformation compilation requires at least one surface coordinate.");
}
}
mfem::ParFiniteElementSpace &SurfaceDeformationCompilationContext::scalarFiniteElementSpace() const noexcept {
return *m_scalarFiniteElementSpace;
}
const field::ScalarBoundaryDofMap &SurfaceDeformationCompilationContext::surfaceDofMap() const noexcept {
return m_surfaceDofMap;
}
NodalRadialSurface::NodalRadialSurface(mfem::Vector referenceCenter)
: m_referenceCenter(std::move(referenceCenter)) {
validate();
}
const mfem::Vector &NodalRadialSurface::referenceCenter() const noexcept {
return m_referenceCenter;
}
SurfaceDeformationDescriptor NodalRadialSurface::descriptor() const noexcept {
return nodalRadialDescriptor(m_referenceCenter.Size());
}
void NodalRadialSurface::validate() const {
if (m_referenceCenter.Size() <= 0) {
throw std::invalid_argument("NodalRadialSurface requires a non-empty reference center.");
}
requireFiniteVector(m_referenceCenter, "NodalRadialSurface reference-center coordinates must be finite.");
}
PreparedNodalRadialSurface::PreparedNodalRadialSurface(
const SurfaceDeformationDescriptor descriptor,
mfem::Vector referenceCenter,
field::ScalarBoundaryDofMap surfaceDofMap,
mfem::Vector radialDirections,
mfem::Vector referenceRadii
)
: m_descriptor(descriptor),
m_referenceCenter(std::move(referenceCenter)),
m_surfaceDofMap(std::move(surfaceDofMap)),
m_radialDirections(std::move(radialDirections)),
m_referenceRadii(std::move(referenceRadii)) {
}
SurfaceDeformationDescriptor PreparedNodalRadialSurface::descriptor() const noexcept {
return m_descriptor;
}
int PreparedNodalRadialSurface::parameterCount() const noexcept {
return m_surfaceDofMap.local_size();
}
long long PreparedNodalRadialSurface::globalParameterCount() const noexcept {
return m_surfaceDofMap.global_size();
}
long long PreparedNodalRadialSurface::globalParameterOffset() const noexcept {
return m_surfaceDofMap.global_offset();
}
int PreparedNodalRadialSurface::spatialDimension() const noexcept {
return m_descriptor.spatialDimension;
}
int PreparedNodalRadialSurface::surfaceDisplacementSize() const noexcept {
return spatialDimension() * parameterCount();
}
long long PreparedNodalRadialSurface::globalSurfaceDisplacementSize() const noexcept {
return static_cast<long long>(spatialDimension()) * globalParameterCount();
}
long long PreparedNodalRadialSurface::globalSurfaceDisplacementOffset() const noexcept {
return static_cast<long long>(spatialDimension()) * globalParameterOffset();
}
int PreparedNodalRadialSurface::surfaceDisplacementDof(
const int parameterDof,
const int component
) const {
if (parameterDof < 0 || parameterDof >= parameterCount()) {
throw std::out_of_range("Parameter DOF is outside PreparedNodalRadialSurface.");
}
if (component < 0 || component >= spatialDimension()) {
throw std::out_of_range("Surface-displacement component is outside PreparedNodalRadialSurface.");
}
return spatialDimension() * parameterDof + component;
}
double PreparedNodalRadialSurface::radialDirection(
const int parameterDof,
const int component
) const {
return m_radialDirections(surfaceDisplacementDof(parameterDof, component));
}
double PreparedNodalRadialSurface::referenceRadius(const int parameterDof) const {
if (parameterDof < 0 || parameterDof >= parameterCount()) {
throw std::out_of_range("Parameter DOF is outside PreparedNodalRadialSurface.");
}
return m_referenceRadii(parameterDof);
}
const mfem::Vector &PreparedNodalRadialSurface::referenceCenter() const noexcept {
return m_referenceCenter;
}
const field::ScalarBoundaryDofMap &PreparedNodalRadialSurface::surfaceDofMap() const noexcept {
return m_surfaceDofMap;
}
void PreparedNodalRadialSurface::buildSurfaceDisplacement(
const mfem::Vector &parameters,
mfem::Vector &surfaceDisplacement
) const {
requireParameterSize(parameters);
requireSurfaceDisplacementSize(surfaceDisplacement);
for (int parameterDof = 0; parameterDof < parameterCount(); ++parameterDof) {
for (int component = 0; component < spatialDimension(); ++component) {
const int surfaceDof = spatialDimension() * parameterDof + component;
surfaceDisplacement(surfaceDof) = parameters(parameterDof) * m_radialDirections(surfaceDof);
}
}
}
void PreparedNodalRadialSurface::applyJacobian(
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
mfem::Vector &surfaceDisplacementDirection
) const {
requireParameterSize(parameters);
requireParameterSize(parameterDirection);
requireSurfaceDisplacementSize(surfaceDisplacementDirection);
for (int parameterDof = 0; parameterDof < parameterCount(); ++parameterDof) {
for (int component = 0; component < spatialDimension(); ++component) {
const int surfaceDof = spatialDimension() * parameterDof + component;
surfaceDisplacementDirection(surfaceDof) =
parameterDirection(parameterDof) * m_radialDirections(surfaceDof);
}
}
}
void PreparedNodalRadialSurface::applyJacobianTranspose(
const mfem::Vector &parameters,
const mfem::Vector &surfaceDisplacementDual,
mfem::Vector &parameterDual
) const {
requireParameterSize(parameters);
requireSurfaceDisplacementSize(surfaceDisplacementDual);
requireParameterSize(parameterDual);
for (int parameterDof = 0; parameterDof < parameterCount(); ++parameterDof) {
double radialWork = 0.0;
for (int component = 0; component < spatialDimension(); ++component) {
const int surfaceDof = spatialDimension() * parameterDof + component;
radialWork += m_radialDirections(surfaceDof) * surfaceDisplacementDual(surfaceDof);
}
parameterDual(parameterDof) = radialWork;
}
}
void PreparedNodalRadialSurface::applyPullbackDerivative(
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
const mfem::Vector &surfaceDisplacementDual,
mfem::Vector &parameterDualAction
) const {
requireParameterSize(parameters);
requireParameterSize(parameterDirection);
requireSurfaceDisplacementSize(surfaceDisplacementDual);
requireParameterSize(parameterDualAction);
parameterDualAction = 0.0;
}
void PreparedNodalRadialSurface::requireParameterSize(const mfem::Vector &parameters) const {
if (parameters.Size() != parameterCount()) {
throw std::invalid_argument(
std::format(
"Nodal radial parameter vector has size {}, but the prepared surface requires {}.",
parameters.Size(), parameterCount()
)
);
}
}
void PreparedNodalRadialSurface::requireSurfaceDisplacementSize(const mfem::Vector &surfaceDisplacement) const {
if (surfaceDisplacement.Size() != surfaceDisplacementSize()) {
throw std::invalid_argument(
std::format(
"Surface displacement vector has size {}, but the prepared nodal radial surface requires {}.",
surfaceDisplacement.Size(), surfaceDisplacementSize()
)
);
}
}
PreparedNodalRadialSurface compileSurfaceDeformationPrescription(
const NodalRadialSurface &prescription,
const SurfaceDeformationCompilationContext &context
) {
prescription.validate();
mfem::ParFiniteElementSpace &scalarSpace = context.scalarFiniteElementSpace();
const mfem::Mesh *mesh = scalarSpace.GetMesh();
if (mesh == nullptr) {
throw std::invalid_argument("Nodal radial surface compilation requires a reference mesh.");
}
if (prescription.referenceCenter().Size() != mesh->SpaceDimension()) {
throw std::invalid_argument(
std::format(
"NodalRadialSurface reference center has dimension {}, but the reference mesh has spatial "
"dimension {}.",
prescription.referenceCenter().Size(), mesh->SpaceDimension()
)
);
}
const field::ScalarBoundaryDofMap &surfaceDofMap = context.surfaceDofMap();
const int parameterCount = surfaceDofMap.local_size();
const int spatialDimension = mesh->SpaceDimension();
mfem::Vector referencePositions(spatialDimension * parameterCount);
mfem::ParGridFunction coordinateField(&scalarSpace);
for (int component = 0; component < spatialDimension; ++component) {
mfem::FunctionCoefficient coordinateCoefficient([component](const mfem::Vector &position) {
return position(component);
});
coordinateField.ProjectCoefficient(coordinateCoefficient);
mfem::Vector coordinateTrueDofs;
coordinateField.GetTrueDofs(coordinateTrueDofs);
const mfem::Vector surfaceCoordinates = surfaceDofMap.gather(coordinateTrueDofs);
for (int parameterDof = 0; parameterDof < parameterCount; ++parameterDof) {
referencePositions(spatialDimension * parameterDof + component) = surfaceCoordinates(parameterDof);
}
}
mfem::Vector radialDirections(referencePositions.Size());
mfem::Vector referenceRadii(parameterCount);
for (int parameterDof = 0; parameterDof < parameterCount; ++parameterDof) {
double radiusSquared = 0.0;
for (int component = 0; component < spatialDimension; ++component) {
const int surfaceDof = spatialDimension * parameterDof + component;
const double radialCoordinate =
referencePositions(surfaceDof) - prescription.referenceCenter()(component);
radialDirections(surfaceDof) = radialCoordinate;
radiusSquared += radialCoordinate * radialCoordinate;
}
const double radius = std::sqrt(radiusSquared);
if (!std::isfinite(radius) || radius <= 0.0) {
throw std::invalid_argument(
"Every nodal radial surface coordinate must have a finite positive distance from the reference "
"center."
);
}
referenceRadii(parameterDof) = radius;
for (int component = 0; component < spatialDimension; ++component) {
radialDirections(spatialDimension * parameterDof + component) /= radius;
}
}
return PreparedNodalRadialSurface(
nodalRadialDescriptor(spatialDimension), prescription.referenceCenter(), surfaceDofMap,
std::move(radialDirections), std::move(referenceRadii)
);
}
} // namespace mean_field::deformation

File diff suppressed because it is too large Load Diff

View File

@@ -45,15 +45,33 @@ namespace mean_field::fem {
stroid::refinement::UniformRefinement(fem.smesh, extraRefine);
}
if (fem.smesh.mesh == nullptr || fem.smesh.reference_mesh == nullptr) {
throw std::runtime_error("A STROID mesh requires paired physical and logical reference meshes.");
}
int mpiSize = 1;
MPI_Comm_size(MPI_COMM_WORLD, &mpiSize);
const std::unique_ptr<int[]> meshPartitioning(fem.smesh.mesh->GeneratePartitioning(mpiSize, 1));
fem.mesh = std::make_unique<mfem::ParMesh>(MPI_COMM_WORLD, *fem.smesh.mesh, meshPartitioning.get(), 1);
fem.logicalReferenceMesh =
std::make_unique<mfem::ParMesh>(MPI_COMM_WORLD, *fem.smesh.reference_mesh, meshPartitioning.get(), 1);
fem.mesh->EnsureNodes();
if (fem.logicalReferenceMesh->GetNE() != fem.mesh->GetNE()) {
throw std::runtime_error("The physical and logical reference meshes have incompatible local elements.");
}
for (int element = 0; element < fem.mesh->GetNE(); ++element) {
if (fem.logicalReferenceMesh->GetElementGeometry(element) != fem.mesh->GetElementGeometry(element) ||
fem.logicalReferenceMesh->GetAttribute(element) != fem.mesh->GetAttribute(element)) {
throw std::runtime_error(
"The physical and logical reference meshes do not preserve element correspondence."
);
}
}
// =====================================================================
// Section 2: Exterior compactification coordinate
// =====================================================================
@@ -185,6 +203,17 @@ namespace mean_field::fem {
*fem.displacement = 0.0;
// ---------------------------------------------------------------------
// Surface deformation: scalar H1 coordinates on StellarSurface.
//
// This ambient scalar space exists only to define the surface basis
// and owned true-DOF topology. Interior scalar DOFs are not nonlinear
// unknowns.
// ---------------------------------------------------------------------
fem.surfaceDeformationFes =
std::make_unique<mfem::ParFiniteElementSpace>(fem.mesh.get(), fem.displacementFec.get());
// ---------------------------------------------------------------------
// Density: scalar discontinuous L2
// ---------------------------------------------------------------------

View File

@@ -2,6 +2,7 @@ module;
#include <array>
#include <cmath>
#include <cstdint>
#include <utility>
#include <mfem.hpp>
@@ -21,29 +22,23 @@ namespace {
);
}
[[nodiscard]] mfem::Vector make_computational_origin(const mfem::ParMesh &mesh) {
mfem::Vector origin(mesh.SpaceDimension());
origin = 0.0;
return origin;
}
[[nodiscard]] mean_field::operators::StellarEquilibriumLayout make_layout(
const mean_field::field::FieldDofMap &densityMap,
const mean_field::field::FieldDofMap &displacementMap,
const int surfaceDeformationParameterCount,
const mean_field::field::FieldDofMap &gravityFluxMap,
const mean_field::field::FieldDofMap &gravityPotentialMap,
const mean_field::field::FieldDofMap &enthalpyMap
) {
using Form = mean_field::utils::blocks::barotropic_equilibrium_form;
using Form = mean_field::utils::blocks::surface_deformed_stellar_equilibrium_form;
const std::array<int, Form::value_block_count> valueSizes{
densityMap.reduced_size(), displacementMap.reduced_size(), gravityFluxMap.reduced_size(),
densityMap.reduced_size(), surfaceDeformationParameterCount, gravityFluxMap.reduced_size(),
gravityPotentialMap.reduced_size(), enthalpyMap.reduced_size(), 1
};
const std::array<int, Form::residual_block_count> residualSizes{
gravityFluxMap.reduced_size(), gravityPotentialMap.reduced_size(), densityMap.reduced_size(),
displacementMap.reduced_size(), enthalpyMap.reduced_size(), 1
surfaceDeformationParameterCount, enthalpyMap.reduced_size(), 1
};
return {valueSizes, residualSizes};
@@ -182,11 +177,13 @@ namespace {
);
}
[[nodiscard]] mean_field::operators::context::gravity_field::GravityFieldRevisions
make_gravity_revisions(const mean_field::operators::StellarEquilibriumDependencies &dependencies) {
[[nodiscard]] mean_field::operators::context::gravity_field::GravityFieldRevisions make_gravity_revisions(
const mean_field::operators::StellarEquilibriumDependencies &dependencies,
const mean_field::operators::StellarEquilibriumDependencyStamp &generatedDisplacement
) {
return {
.discretization = {.value = dependencies.discretization.revision},
.displacement = {.value = dependencies.displacement.revision},
.displacement = {.value = generatedDisplacement.revision},
.density = {.value = dependencies.density.revision},
.gravity_gradient = {.value = dependencies.gravityGradient.revision},
.gravity_potential = {.value = dependencies.gravityPotential.revision}
@@ -194,26 +191,28 @@ namespace {
}
[[nodiscard]] mean_field::operators::context::barotropic::BarotropicClosureDependencies
make_barotropic_closure_dependencies(const mean_field::operators::StellarEquilibriumDependencies &dependencies) {
make_barotropic_closure_dependencies(
const mean_field::operators::StellarEquilibriumDependencies &dependencies,
const mean_field::operators::StellarEquilibriumDependencyStamp &generatedDisplacement
) {
return {
.discretization =
{.identity = dependencies.discretization.identity, .revision = dependencies.discretization.revision},
.density = {.identity = dependencies.density.identity, .revision = dependencies.density.revision},
.enthalpy = {.identity = dependencies.enthalpy.identity, .revision = dependencies.enthalpy.revision},
.displacement = {
.identity = dependencies.displacement.identity, .revision = dependencies.displacement.revision
}
.displacement = {.identity = generatedDisplacement.identity, .revision = generatedDisplacement.revision}
};
}
[[nodiscard]] mean_field::operators::DisplacementResidualDependencies
make_displacement_dependencies(const mean_field::operators::StellarEquilibriumDependencies &dependencies) {
[[nodiscard]] mean_field::operators::DisplacementResidualDependencies make_displacement_dependencies(
const mean_field::operators::StellarEquilibriumDependencies &dependencies,
const mean_field::operators::StellarEquilibriumDependencyStamp &generatedDisplacement
) {
return {
.discretization =
{.identity = dependencies.discretization.identity, .revision = dependencies.discretization.revision},
.density = {.identity = dependencies.density.identity, .revision = dependencies.density.revision},
.displacement =
{.identity = dependencies.displacement.identity, .revision = dependencies.displacement.revision},
.displacement = {.identity = generatedDisplacement.identity, .revision = generatedDisplacement.revision},
.gravityGradient =
{.identity = dependencies.gravityGradient.identity, .revision = dependencies.gravityGradient.revision},
.enthalpy = {.identity = dependencies.enthalpy.identity, .revision = dependencies.enthalpy.revision},
@@ -222,7 +221,10 @@ namespace {
}
[[nodiscard]] mean_field::operators::context::hydrostatic::HydrostaticEquilibriumDependencies
make_hydrostatic_dependencies(const mean_field::operators::StellarEquilibriumDependencies &dependencies) {
make_hydrostatic_dependencies(
const mean_field::operators::StellarEquilibriumDependencies &dependencies,
const mean_field::operators::StellarEquilibriumDependencyStamp &generatedDisplacement
) {
return {
.discretization =
{.identity = dependencies.discretization.identity, .revision = dependencies.discretization.revision},
@@ -230,8 +232,7 @@ namespace {
.gravityPotential =
{.identity = dependencies.gravityPotential.identity,
.revision = dependencies.gravityPotential.revision},
.displacement =
{.identity = dependencies.displacement.identity, .revision = dependencies.displacement.revision},
.displacement = {.identity = generatedDisplacement.identity, .revision = generatedDisplacement.revision},
.rotation = {.identity = dependencies.rotation.identity, .revision = dependencies.rotation.revision},
.bernoulliConstant = {
.identity = dependencies.bernoulliConstant.identity, .revision = dependencies.bernoulliConstant.revision
@@ -239,14 +240,15 @@ namespace {
};
}
[[nodiscard]] mean_field::operators::MassNormalizationDependencies
make_mass_dependencies(const mean_field::operators::StellarEquilibriumDependencies &dependencies) {
[[nodiscard]] mean_field::operators::MassNormalizationDependencies make_mass_dependencies(
const mean_field::operators::StellarEquilibriumDependencies &dependencies,
const mean_field::operators::StellarEquilibriumDependencyStamp &generatedDisplacement
) {
return {
.discretization =
{.identity = dependencies.discretization.identity, .revision = dependencies.discretization.revision},
.density = {.identity = dependencies.density.identity, .revision = dependencies.density.revision},
.displacement =
{.identity = dependencies.displacement.identity, .revision = dependencies.displacement.revision},
.displacement = {.identity = generatedDisplacement.identity, .revision = generatedDisplacement.revision},
.targetMass = {.identity = dependencies.targetMass.identity, .revision = dependencies.targetMass.revision}
};
}
@@ -254,20 +256,24 @@ namespace {
namespace mean_field::operators {
struct PreparedStellarEquilibriumOperator::ConstructionData {
deformation::PreparedDomainDeformationRuntime domainDeformation;
field::FieldDofMap densityMap;
field::FieldDofMap displacementMap;
field::FieldDofMap gravityFluxMap;
field::FieldDofMap gravityPotentialMap;
field::FieldDofMap enthalpyMap;
field::FieldBoundaryDofMap pressureSurfaceRows;
field::FieldPointDofMap centerDisplacementRows;
StellarEquilibriumLayout layout;
mfem::Array<int> gravityStateOffsets;
mfem::Array<int> gravityResidualOffsets;
explicit ConstructionData(fem::FEM &f)
: densityMap(
ConstructionData(
fem::FEM &f,
deformation::PreparedDomainDeformationRuntime preparedDomainDeformation
)
: domainDeformation(std::move(preparedDomainDeformation)),
densityMap(
field::make_field_dof_map<
field::Density,
DomainSchema>(*f.densityFes)
@@ -301,17 +307,9 @@ namespace mean_field::operators {
enthalpyMap
)
),
centerDisplacementRows(
field::make_field_point_dof_map<field::Displacement>(
*f.displacementFes,
displacementMap,
make_computational_origin(*f.mesh),
1.0e-12
)
),
layout(make_layout(
densityMap,
displacementMap,
domainDeformation.parameterCount(),
gravityFluxMap,
gravityPotentialMap,
enthalpyMap
@@ -329,10 +327,12 @@ namespace mean_field::operators {
}
};
PreparedStellarEquilibriumOperator::ConstructionData
PreparedStellarEquilibriumOperator::MakeConstructionData(fem::FEM &f) {
PreparedStellarEquilibriumOperator::ConstructionData PreparedStellarEquilibriumOperator::MakeConstructionData(
fem::FEM &f,
deformation::PreparedDomainDeformationRuntime domainDeformation
) {
verify_coupled_discretization(f);
return ConstructionData(f);
return ConstructionData(f, std::move(domainDeformation));
}
PreparedStellarEquilibriumOperator::PreparedStellarEquilibriumOperator(
@@ -340,7 +340,8 @@ namespace mean_field::operators {
const mapping::DomainMapper &domainMapper,
const eos::Polytrope &equationOfState,
const double targetMass,
const PressureSurfaceConstraintView surfaceConstraint
const PressureSurfaceConstraintView surfaceConstraint,
deformation::PreparedDomainDeformationRuntime domainDeformation
)
: PreparedStellarEquilibriumOperator(
f,
@@ -348,7 +349,10 @@ namespace mean_field::operators {
equationOfState,
targetMass,
surfaceConstraint,
MakeConstructionData(f)
MakeConstructionData(
f,
std::move(domainDeformation)
)
) {
}
@@ -408,7 +412,7 @@ namespace mean_field::operators {
constructionData.pressureSurfaceRows,
surfaceConstraint
),
m_centeringConstraintOperator(constructionData.centerDisplacementRows),
m_domainDeformation(std::move(constructionData.domainDeformation)),
m_targetMass(targetMass) {
MFEM_VERIFY(
std::isfinite(m_targetMass) && m_targetMass > 0.0,
@@ -420,12 +424,35 @@ namespace mean_field::operators {
"PreparedStellarEquilibriumOperator has inconsistent block dimensions."
);
MFEM_VERIFY(
m_domainDeformation.volumeDisplacementSize() == constructionData.displacementMap.reduced_size(),
"The domain-deformation output does not match the coupled displacement discretization."
);
m_generatedDisplacementDependency.identity =
static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&m_domainDeformation));
m_gravityState.SetSize(m_gravityStateOffsets.Last());
m_gravityDirection.SetSize(m_gravityStateOffsets.Last());
m_surfaceDeformationParameters.SetSize(m_domainDeformation.parameterCount());
m_generatedVolumeDisplacement.SetSize(m_domainDeformation.volumeDisplacementSize());
m_fullMechanicalResidual.SetSize(m_domainDeformation.volumeDisplacementSize());
m_volumeDisplacementDirection.SetSize(m_domainDeformation.volumeDisplacementSize());
m_fullMechanicalAction.SetSize(m_domainDeformation.volumeDisplacementSize());
m_surfaceShapeAction.SetSize(m_domainDeformation.parameterCount());
m_pullbackDerivativeAction.SetSize(m_domainDeformation.parameterCount());
m_gravityState = 0.0;
m_gravityDirection = 0.0;
m_surfaceDeformationParameters = 0.0;
m_generatedVolumeDisplacement = 0.0;
m_fullMechanicalResidual = 0.0;
m_volumeDisplacementDirection = 0.0;
m_fullMechanicalAction = 0.0;
m_surfaceShapeAction = 0.0;
m_pullbackDerivativeAction = 0.0;
}
PreparedStellarEquilibriumReport PreparedStellarEquilibriumOperator::Prepare(
@@ -448,8 +475,8 @@ namespace mean_field::operators {
m_preparedDependencies.density, dependencies.density, "The density revision cannot move backwards."
);
validate_dependency_transition(
m_preparedDependencies.displacement, dependencies.displacement,
"The displacement revision cannot move backwards."
m_preparedDependencies.surfaceDeformation, dependencies.surfaceDeformation,
"The surface-deformation revision cannot move backwards."
);
validate_dependency_transition(
m_preparedDependencies.gravityGradient, dependencies.gravityGradient,
@@ -477,10 +504,10 @@ namespace mean_field::operators {
m_isPrepared = false;
using Form = utils::blocks::barotropic_equilibrium_form;
using Form = utils::blocks::surface_deformed_stellar_equilibrium_form;
constexpr auto densityValue = utils::blocks::get_value_block<Form>(utils::blocks::density_field.mass_term);
constexpr auto displacementValue =
utils::blocks::get_value_block<Form>(utils::blocks::displacement_field.geometry_term);
constexpr auto surfaceDeformationValue =
utils::blocks::get_value_block<Form>(utils::blocks::surface_deformation_field.parameters_term);
constexpr auto gravityGradientValue =
utils::blocks::get_value_block<Form>(utils::blocks::gravity_field.gradient_term);
constexpr auto gravityPotentialValue =
@@ -491,48 +518,64 @@ namespace mean_field::operators {
utils::blocks::get_value_block<Form>(utils::blocks::barotropic_constant_field.mass_normalization_term);
const mfem::Vector reducedDensity = make_value_view(state, m_layout, densityValue);
const mfem::Vector displacement = make_value_view(state, m_layout, displacementValue);
const mfem::Vector surfaceDeformationParameters = make_value_view(state, m_layout, surfaceDeformationValue);
const mfem::Vector gravityGradient = make_value_view(state, m_layout, gravityGradientValue);
const mfem::Vector gravityPotential = make_value_view(state, m_layout, gravityPotentialValue);
const mfem::Vector reducedEnthalpy = make_value_view(state, m_layout, enthalpyValue);
const mfem::Vector bernoulli = make_value_view(state, m_layout, bernoulliValue);
pack_gravity_vector(
m_gravityState, m_gravityStateOffsets, reducedDensity, displacement, gravityGradient, gravityPotential
);
const bool generatedGeometryChanged =
!wasPrepared || dependencies.discretization != m_preparedDependencies.discretization ||
dependencies.surfaceDeformation != m_preparedDependencies.surfaceDeformation;
PreparedStellarEquilibriumReport report;
if (generatedGeometryChanged) {
m_surfaceDeformationParameters = surfaceDeformationParameters;
m_generatedGeometryReport = m_domainDeformation.buildValidatedVolumeDisplacement(
m_surfaceDeformationParameters, m_generatedVolumeDisplacement
);
++m_generatedDisplacementDependency.revision;
++m_statistics.generatedGeometryBuilds;
report.generatedVolumeDisplacement = true;
}
report.generatedGeometry = m_generatedGeometryReport;
report.generatedDisplacement = m_generatedDisplacementDependency;
report.gravity = m_gravityOperator.Prepare(m_gravityState, make_gravity_revisions(dependencies));
pack_gravity_vector(
m_gravityState, m_gravityStateOffsets, reducedDensity, m_generatedVolumeDisplacement, gravityGradient,
gravityPotential
);
report.gravity = m_gravityOperator.Prepare(
m_gravityState, make_gravity_revisions(dependencies, m_generatedDisplacementDependency)
);
report.barotropicClosure = m_barotropicClosureOperator.Prepare(
{.density = reducedDensity, .enthalpy = reducedEnthalpy, .displacement = displacement},
make_barotropic_closure_dependencies(dependencies)
{.density = reducedDensity, .enthalpy = reducedEnthalpy, .displacement = m_generatedVolumeDisplacement},
make_barotropic_closure_dependencies(dependencies, m_generatedDisplacementDependency)
);
report.hydrostatic = m_hydrostaticOperator.Prepare(
{.enthalpy = reducedEnthalpy,
.gravityPotential = gravityPotential,
.displacement = displacement,
.displacement = m_generatedVolumeDisplacement,
.bernoulliConstant = bernoulli(0)},
make_hydrostatic_dependencies(dependencies), rotation
make_hydrostatic_dependencies(dependencies, m_generatedDisplacementDependency), rotation
);
report.displacement = m_displacementOperator.Prepare(
{.enthalpy = reducedEnthalpy}, make_displacement_dependencies(dependencies), rotation
{.enthalpy = reducedEnthalpy},
make_displacement_dependencies(dependencies, m_generatedDisplacementDependency), rotation
);
report.massNormalization =
m_massNormalizationOperator.Prepare({.targetMass = m_targetMass}, make_mass_dependencies(dependencies));
report.massNormalization = m_massNormalizationOperator.Prepare(
{.targetMass = m_targetMass}, make_mass_dependencies(dependencies, m_generatedDisplacementDependency)
);
report.surfaceConstraint = m_surfaceConstraintOperator.Prepare(
reducedEnthalpy, !wasPrepared || dependencies.enthalpy != m_preparedDependencies.enthalpy
);
report.centeringConstraint = m_centeringConstraintOperator.Prepare(
displacement, !wasPrepared || dependencies.displacement != m_preparedDependencies.displacement
);
const bool dependenciesChanged = !wasPrepared || dependencies != m_preparedDependencies;
if (dependenciesChanged || report.DidAnyChildWork()) {
AssembleResidual();
@@ -545,7 +588,7 @@ namespace mean_field::operators {
}
void PreparedStellarEquilibriumOperator::AssembleResidual() {
using Form = utils::blocks::barotropic_equilibrium_form;
using Form = utils::blocks::surface_deformed_stellar_equilibrium_form;
constexpr auto gravityGradientResidual =
utils::blocks::get_residual_block<Form>(utils::blocks::gravity_field.gradient_term);
@@ -553,8 +596,8 @@ namespace mean_field::operators {
utils::blocks::get_residual_block<Form>(utils::blocks::gravity_field.poisson_term);
constexpr auto densityResidual =
utils::blocks::get_residual_block<Form>(utils::blocks::density_field.mass_term);
constexpr auto displacementResidual =
utils::blocks::get_residual_block<Form>(utils::blocks::displacement_field.geometry_term);
constexpr auto surfaceShapeResidual =
utils::blocks::get_residual_block<Form>(utils::blocks::surface_deformation_field.shape_equilibrium_term);
constexpr auto enthalpyResidual =
utils::blocks::get_residual_block<Form>(utils::blocks::enthalpy_field.specific_term);
constexpr auto massResidual =
@@ -562,14 +605,17 @@ namespace mean_field::operators {
mfem::Vector gravity;
mfem::Vector closure;
mfem::Vector displacement;
mfem::Vector surfaceShape;
mfem::Vector hydrostatic;
mfem::Vector mass;
m_gravityOperator.Mult(m_gravityState, gravity);
m_barotropicClosureOperator.BuildResidual(closure);
m_displacementOperator.BuildResidual(displacement);
m_centeringConstraintOperator.ApplyResidualRows(displacement);
m_displacementOperator.BuildResidual(m_fullMechanicalResidual);
surfaceShape.SetSize(m_domainDeformation.parameterCount());
m_domainDeformation.applyJacobianTranspose(
m_surfaceDeformationParameters, m_fullMechanicalResidual, surfaceShape
);
m_hydrostaticOperator.BuildResidual(hydrostatic);
m_surfaceConstraintOperator.ApplyResidualRows(hydrostatic);
m_massNormalizationOperator.BuildResidual(mass);
@@ -599,8 +645,8 @@ namespace mean_field::operators {
m_cachedResidual, m_layout, densityResidual, closure, "The closure residual has the wrong size."
);
assign_residual_block(
m_cachedResidual, m_layout, displacementResidual, displacement,
"The displacement residual has the wrong size."
m_cachedResidual, m_layout, surfaceShapeResidual, surfaceShape,
"The surface-shape residual has the wrong size."
);
assign_residual_block(
@@ -633,11 +679,11 @@ namespace mean_field::operators {
direction, "PreparedStellarEquilibriumOperator received a non-finite Jacobian direction."
);
using Form = utils::blocks::barotropic_equilibrium_form;
using Form = utils::blocks::surface_deformed_stellar_equilibrium_form;
constexpr auto densityValue = utils::blocks::get_value_block<Form>(utils::blocks::density_field.mass_term);
constexpr auto displacementValue =
utils::blocks::get_value_block<Form>(utils::blocks::displacement_field.geometry_term);
constexpr auto surfaceDeformationValue =
utils::blocks::get_value_block<Form>(utils::blocks::surface_deformation_field.parameters_term);
constexpr auto gravityGradientValue =
utils::blocks::get_value_block<Form>(utils::blocks::gravity_field.gradient_term);
constexpr auto gravityPotentialValue =
@@ -653,51 +699,61 @@ namespace mean_field::operators {
utils::blocks::get_residual_block<Form>(utils::blocks::gravity_field.poisson_term);
constexpr auto densityResidual =
utils::blocks::get_residual_block<Form>(utils::blocks::density_field.mass_term);
constexpr auto displacementResidual =
utils::blocks::get_residual_block<Form>(utils::blocks::displacement_field.geometry_term);
constexpr auto surfaceShapeResidual =
utils::blocks::get_residual_block<Form>(utils::blocks::surface_deformation_field.shape_equilibrium_term);
constexpr auto enthalpyResidual =
utils::blocks::get_residual_block<Form>(utils::blocks::enthalpy_field.specific_term);
constexpr auto massResidual =
utils::blocks::get_residual_block<Form>(utils::blocks::barotropic_constant_field.mass_normalization_term);
const mfem::Vector reducedDensityDirection = make_value_view(direction, m_layout, densityValue);
const mfem::Vector displacementDirection = make_value_view(direction, m_layout, displacementValue);
const mfem::Vector surfaceDeformationDirection = make_value_view(direction, m_layout, surfaceDeformationValue);
const mfem::Vector gravityGradientDirection = make_value_view(direction, m_layout, gravityGradientValue);
const mfem::Vector gravityPotentialDirection = make_value_view(direction, m_layout, gravityPotentialValue);
const mfem::Vector reducedEnthalpyDirection = make_value_view(direction, m_layout, enthalpyValue);
const mfem::Vector bernoulliDirection = make_value_view(direction, m_layout, bernoulliValue);
m_domainDeformation.applyJacobian(
m_surfaceDeformationParameters, surfaceDeformationDirection, m_volumeDisplacementDirection
);
pack_gravity_vector(
m_gravityDirection, m_gravityStateOffsets, reducedDensityDirection, displacementDirection,
m_gravityDirection, m_gravityStateOffsets, reducedDensityDirection, m_volumeDisplacementDirection,
gravityGradientDirection, gravityPotentialDirection
);
mfem::Vector gravityAction;
mfem::Vector closureAction;
mfem::Vector displacementAction;
mfem::Vector hydrostaticAction;
mfem::Vector massAction;
m_gravityJacobianOperator.Mult(m_gravityDirection, gravityAction);
m_barotropicClosureOperator.Mult(
reducedDensityDirection, reducedEnthalpyDirection, displacementDirection, closureAction
reducedDensityDirection, reducedEnthalpyDirection, m_volumeDisplacementDirection, closureAction
);
m_displacementOperator.ApplyCompleteJacobianAction(
reducedDensityDirection, displacementDirection, gravityGradientDirection, reducedEnthalpyDirection,
displacementAction
reducedDensityDirection, m_volumeDisplacementDirection, gravityGradientDirection, reducedEnthalpyDirection,
m_fullMechanicalAction
);
m_centeringConstraintOperator.ApplyJacobianRows(displacementDirection, displacementAction);
m_domainDeformation.applyJacobianTranspose(
m_surfaceDeformationParameters, m_fullMechanicalAction, m_surfaceShapeAction
);
m_domainDeformation.applyPullbackDerivative(
m_surfaceDeformationParameters, surfaceDeformationDirection, m_fullMechanicalResidual,
m_pullbackDerivativeAction
);
m_surfaceShapeAction += m_pullbackDerivativeAction;
m_hydrostaticOperator.ApplyCompleteJacobianAction(
reducedEnthalpyDirection, gravityPotentialDirection, bernoulliDirection(0), displacementDirection,
reducedEnthalpyDirection, gravityPotentialDirection, bernoulliDirection(0), m_volumeDisplacementDirection,
hydrostaticAction
);
m_surfaceConstraintOperator.ApplyJacobianRows(reducedEnthalpyDirection, hydrostaticAction);
m_massNormalizationOperator.ApplyCompleteJacobianAction(
reducedDensityDirection, displacementDirection, massAction
reducedDensityDirection, m_volumeDisplacementDirection, massAction
);
action.SetSize(Height());
@@ -725,8 +781,8 @@ namespace mean_field::operators {
action, m_layout, densityResidual, closureAction, "The closure Jacobian action has the wrong size."
);
assign_residual_block(
action, m_layout, displacementResidual, displacementAction,
"The displacement Jacobian action has the wrong size."
action, m_layout, surfaceShapeResidual, m_surfaceShapeAction,
"The surface-shape Jacobian action has the wrong size."
);
assign_residual_block(
@@ -743,8 +799,7 @@ namespace mean_field::operators {
bool PreparedStellarEquilibriumOperator::IsPrepared() const noexcept {
return m_isPrepared && m_gravityContext.IsPrepared() && m_barotropicClosureOperator.IsPrepared() &&
m_hydrostaticOperator.IsPrepared() && m_displacementOperator.IsPrepared() &&
m_massNormalizationOperator.IsPrepared() && m_surfaceConstraintOperator.IsPrepared() &&
m_centeringConstraintOperator.IsPrepared();
m_massNormalizationOperator.IsPrepared() && m_surfaceConstraintOperator.IsPrepared();
}
double PreparedStellarEquilibriumOperator::GetTargetMass() const noexcept {
@@ -808,9 +863,30 @@ namespace mean_field::operators {
return m_surfaceConstraintOperator;
}
const PreparedCenteringConstraint &
PreparedStellarEquilibriumOperator::GetCenteringConstraintOperator() const noexcept {
return m_centeringConstraintOperator;
const deformation::PreparedDomainDeformationRuntime &
PreparedStellarEquilibriumOperator::GetDomainDeformation() const noexcept {
return m_domainDeformation;
}
const mfem::Vector &PreparedStellarEquilibriumOperator::GetSurfaceDeformationParameters() const {
VerifyPrepared();
return m_surfaceDeformationParameters;
}
const mfem::Vector &PreparedStellarEquilibriumOperator::GetGeneratedVolumeDisplacement() const {
VerifyPrepared();
return m_generatedVolumeDisplacement;
}
const mfem::Vector &PreparedStellarEquilibriumOperator::GetFullMechanicalResidual() const {
VerifyPrepared();
return m_fullMechanicalResidual;
}
const StellarEquilibriumDependencyStamp &
PreparedStellarEquilibriumOperator::GetGeneratedDisplacementDependency() const {
VerifyPrepared();
return m_generatedDisplacementDependency;
}
void PreparedStellarEquilibriumOperator::VerifyPrepared() const {

View File

@@ -0,0 +1,111 @@
module;
#include <cstdint>
#include <string_view>
export module mean_field:deformation.descriptors;
export namespace mean_field::deformation {
enum class SurfaceMotionKind : std::uint8_t { Radial, Normal, GeneralVector };
enum class GeometricGaugeTreatment : std::uint8_t {
Retained,
ExcludedByParameterization,
ConstrainedByPrescription
};
enum class InteriorCenterBehavior : std::uint8_t { Unspecified, FixedAtReferenceCenter, DeterminedBySurfaceMotion };
enum class VacuumOuterBoundaryBehavior : std::uint8_t {
Unspecified,
FixedAtReferenceInfinity,
DeterminedBySurfaceMotion
};
struct SurfaceDeformationDescriptor final {
std::string_view name;
int spatialDimension;
SurfaceMotionKind motionKind;
bool linearOnReferenceGeometry;
bool requiresStarShapedReferenceSurface;
bool hasExactDerivativeTranspose;
bool hasExactPullbackDerivative;
GeometricGaugeTreatment translationTreatment;
GeometricGaugeTreatment orientationTreatment;
[[nodiscard]] constexpr bool isValid() const noexcept {
return !name.empty() && spatialDimension > 0;
}
[[nodiscard]] constexpr bool supportsExactNewtonLinearization() const noexcept {
return hasExactDerivativeTranspose && hasExactPullbackDerivative;
}
constexpr bool operator==(const SurfaceDeformationDescriptor &) const = default;
};
struct InteriorDeformationExtensionDescriptor final {
std::string_view name;
int spatialDimension;
bool linearOnReferenceGeometry;
bool requiresRadialFoliation;
bool requiresAuxiliarySolve;
bool hasExactDerivativeTranspose;
bool hasExactPullbackDerivative;
InteriorCenterBehavior centerBehavior;
[[nodiscard]] constexpr bool isValid() const noexcept {
return !name.empty() && spatialDimension > 0 && centerBehavior != InteriorCenterBehavior::Unspecified;
}
[[nodiscard]] constexpr bool supportsExactNewtonLinearization() const noexcept {
return hasExactDerivativeTranspose && hasExactPullbackDerivative;
}
constexpr bool operator==(const InteriorDeformationExtensionDescriptor &) const = default;
};
struct VacuumDeformationExtensionDescriptor final {
std::string_view name;
int spatialDimension;
bool linearOnReferenceGeometry;
bool requiresRadialFoliation;
bool requiresAuxiliarySolve;
bool hasExactDerivativeTranspose;
bool hasExactPullbackDerivative;
VacuumOuterBoundaryBehavior outerBoundaryBehavior;
[[nodiscard]] constexpr bool isValid() const noexcept {
return !name.empty() && spatialDimension > 0 &&
outerBoundaryBehavior != VacuumOuterBoundaryBehavior::Unspecified;
}
[[nodiscard]] constexpr bool supportsExactNewtonLinearization() const noexcept {
return hasExactDerivativeTranspose && hasExactPullbackDerivative;
}
constexpr bool operator==(const VacuumDeformationExtensionDescriptor &) const = default;
};
struct DomainDeformationDescriptor final {
SurfaceDeformationDescriptor surfaceDeformation;
InteriorDeformationExtensionDescriptor stellarInteriorExtension;
VacuumDeformationExtensionDescriptor vacuumExtension;
bool linearOnReferenceGeometry;
bool requiresAuxiliarySolve;
bool hasExactDerivativeTranspose;
bool hasExactPullbackDerivative;
[[nodiscard]] constexpr bool isValid() const noexcept {
return surfaceDeformation.isValid() && stellarInteriorExtension.isValid() && vacuumExtension.isValid() &&
surfaceDeformation.spatialDimension == stellarInteriorExtension.spatialDimension &&
surfaceDeformation.spatialDimension == vacuumExtension.spatialDimension;
}
[[nodiscard]] constexpr bool supportsExactNewtonLinearization() const noexcept {
return hasExactDerivativeTranspose && hasExactPullbackDerivative;
}
constexpr bool operator==(const DomainDeformationDescriptor &) const = default;
};
} // namespace mean_field::deformation

View File

@@ -0,0 +1,870 @@
module;
#include <algorithm>
#include <cmath>
#include <compare>
#include <concepts>
#include <cstdint>
#include <limits>
#include <memory>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <vector>
#include <mfem.hpp>
#include <mpi.h>
export module mean_field:deformation.domain_deformation;
export import :deformation.interior_extension;
export import :deformation.nodal_radial_surface;
export import :deformation.radial_extensions;
export import :deformation.surface_prescription;
export import :deformation.vacuum_extension;
export import :fem;
export import :field.mfem;
export import :utils.domain;
export namespace mean_field::deformation {
enum class VolumeDeformationOwner : std::uint8_t { StellarInterior, Vacuum };
struct DomainDeformationDiscretizationDependencies final {
const mfem::Mesh *physicalMeshIdentity{nullptr};
const mfem::ParMesh *logicalReferenceMeshIdentity{nullptr};
const mfem::ParFiniteElementSpace *surfaceScalarSpaceIdentity{nullptr};
const mfem::ParFiniteElementSpace *volumeDisplacementSpaceIdentity{nullptr};
long physicalMeshSequence{-1};
long logicalReferenceMeshSequence{-1};
long surfaceScalarSpaceSequence{-1};
long volumeDisplacementSpaceSequence{-1};
[[nodiscard]] bool isCurrent() const noexcept {
return physicalMeshIdentity != nullptr && logicalReferenceMeshIdentity != nullptr &&
surfaceScalarSpaceIdentity != nullptr && volumeDisplacementSpaceIdentity != nullptr &&
physicalMeshIdentity->GetSequence() == physicalMeshSequence &&
logicalReferenceMeshIdentity->GetSequence() == logicalReferenceMeshSequence &&
surfaceScalarSpaceIdentity->GetSequence() == surfaceScalarSpaceSequence &&
volumeDisplacementSpaceIdentity->GetSequence() == volumeDisplacementSpaceSequence;
}
};
struct DomainDeformationCompositionReport final {
int scalarTrueDofCount{0};
int stellarInteriorOwnedScalarDofCount{0};
int vacuumOwnedScalarDofCount{0};
int sharedSurfaceScalarDofCount{0};
[[nodiscard]] constexpr int assignedScalarDofCount() const noexcept {
return stellarInteriorOwnedScalarDofCount + vacuumOwnedScalarDofCount;
}
constexpr auto operator<=>(const DomainDeformationCompositionReport &) const = default;
};
struct DomainDeformationGeometryReport final {
double minimumJacobianDeterminant{std::numeric_limits<double>::infinity()};
[[nodiscard]] bool isOrientationPreserving(const double determinantFloor = 0.0) const noexcept {
return std::isfinite(minimumJacobianDeterminant) && std::isfinite(determinantFloor) &&
determinantFloor >= 0.0 && minimumJacobianDeterminant > determinantFloor;
}
};
struct PreparedDomainDeformationActionStatistics final {
std::uint64_t volumeBuildApplications{0};
std::uint64_t jacobianApplications{0};
std::uint64_t jacobianTransposeApplications{0};
std::uint64_t pullbackDerivativeApplications{0};
std::uint64_t geometryInspections{0};
constexpr auto operator<=>(const PreparedDomainDeformationActionStatistics &) const = default;
};
template <typename Candidate>
concept PreparedDomainDeformationOperator = requires(
const std::remove_cvref_t<Candidate> &preparedDeformation,
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
const mfem::Vector &volumeDisplacementDual,
mfem::Vector &volumeDisplacement,
mfem::Vector &parameterDual
) {
{ preparedDeformation.descriptor() } noexcept -> std::same_as<DomainDeformationDescriptor>;
{ preparedDeformation.parameterCount() } noexcept -> std::same_as<int>;
{ preparedDeformation.surfaceDisplacementSize() } noexcept -> std::same_as<int>;
{ preparedDeformation.volumeDisplacementSize() } noexcept -> std::same_as<int>;
{ preparedDeformation.buildVolumeDisplacement(parameters, volumeDisplacement) } -> std::same_as<void>;
{ preparedDeformation.applyJacobian(parameters, parameterDirection, volumeDisplacement) } -> std::same_as<void>;
{
preparedDeformation.applyJacobianTranspose(parameters, volumeDisplacementDual, parameterDual)
} -> std::same_as<void>;
{
preparedDeformation.applyPullbackDerivative(
parameters, parameterDirection, volumeDisplacementDual, parameterDual
)
} -> std::same_as<void>;
};
template <
PreparedSurfaceDeformationPrescription PreparedSurface,
PreparedInteriorDeformationExtension PreparedInterior,
PreparedVacuumDeformationExtension PreparedVacuum>
class PreparedDomainDeformation final {
public:
PreparedDomainDeformation(
PreparedSurface preparedSurface,
PreparedInterior preparedInterior,
PreparedVacuum preparedVacuum,
mfem::ParFiniteElementSpace &surfaceScalarSpace,
mfem::ParFiniteElementSpace &volumeDisplacementSpace,
mfem::ParMesh &logicalReferenceMesh
)
: m_surface(std::move(preparedSurface)),
m_interior(std::move(preparedInterior)),
m_vacuum(std::move(preparedVacuum)),
m_volumeDisplacementSpace(&volumeDisplacementSpace),
m_descriptor(makeDescriptor(
m_surface,
m_interior,
m_vacuum
)),
m_surfaceDisplacementWorkspace(surfaceDisplacementSize()),
m_surfaceDirectionWorkspace(surfaceDisplacementSize()),
m_interiorVolumeWorkspace(volumeDisplacementSize()),
m_vacuumVolumeWorkspace(volumeDisplacementSize()),
m_interiorVolumeDualWorkspace(volumeDisplacementSize()),
m_vacuumVolumeDualWorkspace(volumeDisplacementSize()),
m_interiorSurfaceDualWorkspace(surfaceDisplacementSize()),
m_vacuumSurfaceDualWorkspace(surfaceDisplacementSize()),
m_surfaceDualWorkspace(surfaceDisplacementSize()),
m_interiorSurfacePullbackWorkspace(surfaceDisplacementSize()),
m_vacuumSurfacePullbackWorkspace(surfaceDisplacementSize()),
m_surfacePullbackWorkspace(surfaceDisplacementSize()),
m_parameterPullbackWorkspace(parameterCount()),
m_volumeGridFunctionWorkspace(std::make_unique<mfem::ParGridFunction>(&volumeDisplacementSpace)) {
validateCompatibility(surfaceScalarSpace, volumeDisplacementSpace, logicalReferenceMesh);
compileOwnership();
const mfem::Mesh *physicalMesh = volumeDisplacementSpace.GetMesh();
m_discretizationDependencies = {
.physicalMeshIdentity = physicalMesh,
.logicalReferenceMeshIdentity = &logicalReferenceMesh,
.surfaceScalarSpaceIdentity = &surfaceScalarSpace,
.volumeDisplacementSpaceIdentity = &volumeDisplacementSpace,
.physicalMeshSequence = physicalMesh->GetSequence(),
.logicalReferenceMeshSequence = logicalReferenceMesh.GetSequence(),
.surfaceScalarSpaceSequence = surfaceScalarSpace.GetSequence(),
.volumeDisplacementSpaceSequence = volumeDisplacementSpace.GetSequence()
};
}
PreparedDomainDeformation(const PreparedDomainDeformation &) = delete;
PreparedDomainDeformation &operator=(const PreparedDomainDeformation &) = delete;
PreparedDomainDeformation(PreparedDomainDeformation &&) noexcept = default;
PreparedDomainDeformation &operator=(PreparedDomainDeformation &&) noexcept = default;
[[nodiscard]] DomainDeformationDescriptor descriptor() const noexcept {
return m_descriptor;
}
[[nodiscard]] int parameterCount() const noexcept {
return m_surface.parameterCount();
}
[[nodiscard]] int surfaceDisplacementSize() const noexcept {
return m_surface.surfaceDisplacementSize();
}
[[nodiscard]] int volumeDisplacementSize() const noexcept {
return m_interior.interiorDisplacementSize();
}
[[nodiscard]] int scalarTrueDofCount() const noexcept {
return m_interior.scalarTrueDofCount();
}
[[nodiscard]] int spatialDimension() const noexcept {
return m_descriptor.surfaceDeformation.spatialDimension;
}
[[nodiscard]] VolumeDeformationOwner volumeOwner(const int scalarTrueDof) const {
requireScalarTrueDof(scalarTrueDof);
return m_volumeOwners[static_cast<std::size_t>(scalarTrueDof)];
}
[[nodiscard]] bool isSharedSurfaceDof(const int scalarTrueDof) const {
requireScalarTrueDof(scalarTrueDof);
return m_interior.hasStellarSupport(scalarTrueDof) && m_vacuum.hasVacuumSupport(scalarTrueDof);
}
[[nodiscard]] const DomainDeformationCompositionReport &compositionReport() const noexcept {
return m_compositionReport;
}
[[nodiscard]] const DomainDeformationDiscretizationDependencies &discretizationDependencies() const noexcept {
return m_discretizationDependencies;
}
[[nodiscard]] bool matchesCurrentDiscretization() const noexcept {
return m_discretizationDependencies.isCurrent();
}
[[nodiscard]] const PreparedDomainDeformationActionStatistics &actionStatistics() const noexcept {
return m_actionStatistics;
}
[[nodiscard]] const PreparedSurface &surfaceDeformationPrescription() const noexcept {
return m_surface;
}
[[nodiscard]] const PreparedInterior &stellarInteriorExtension() const noexcept {
return m_interior;
}
[[nodiscard]] const PreparedVacuum &vacuumExtension() const noexcept {
return m_vacuum;
}
void buildVolumeDisplacement(
const mfem::Vector &parameters,
mfem::Vector &volumeDisplacement
) const {
requireCurrentDiscretization();
requireParameterSize(parameters);
requireVolumeSize(volumeDisplacement);
m_surface.buildSurfaceDisplacement(parameters, m_surfaceDisplacementWorkspace);
m_interior.buildInteriorDisplacement(m_surfaceDisplacementWorkspace, m_interiorVolumeWorkspace);
m_vacuum.buildVacuumDisplacement(m_surfaceDisplacementWorkspace, m_vacuumVolumeWorkspace);
mergeVolumeFields(m_interiorVolumeWorkspace, m_vacuumVolumeWorkspace, volumeDisplacement);
++m_actionStatistics.volumeBuildApplications;
}
void applyJacobian(
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
mfem::Vector &volumeDisplacementDirection
) const {
requireCurrentDiscretization();
requireParameterSize(parameters);
requireParameterSize(parameterDirection);
requireVolumeSize(volumeDisplacementDirection);
m_surface.buildSurfaceDisplacement(parameters, m_surfaceDisplacementWorkspace);
m_surface.applyJacobian(parameters, parameterDirection, m_surfaceDirectionWorkspace);
m_interior.applyJacobian(
m_surfaceDisplacementWorkspace, m_surfaceDirectionWorkspace, m_interiorVolumeWorkspace
);
m_vacuum.applyJacobian(
m_surfaceDisplacementWorkspace, m_surfaceDirectionWorkspace, m_vacuumVolumeWorkspace
);
mergeVolumeFields(m_interiorVolumeWorkspace, m_vacuumVolumeWorkspace, volumeDisplacementDirection);
++m_actionStatistics.jacobianApplications;
}
void applyJacobianTranspose(
const mfem::Vector &parameters,
const mfem::Vector &volumeDisplacementDual,
mfem::Vector &parameterDual
) const {
requireCurrentDiscretization();
requireParameterSize(parameters);
requireVolumeSize(volumeDisplacementDual);
requireParameterSize(parameterDual);
m_surface.buildSurfaceDisplacement(parameters, m_surfaceDisplacementWorkspace);
splitVolumeDual(volumeDisplacementDual);
applyExtensionTransposes();
m_surface.applyJacobianTranspose(parameters, m_surfaceDualWorkspace, parameterDual);
++m_actionStatistics.jacobianTransposeApplications;
}
void applyPullbackDerivative(
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
const mfem::Vector &volumeDisplacementDual,
mfem::Vector &parameterDualAction
) const {
requireCurrentDiscretization();
requireParameterSize(parameters);
requireParameterSize(parameterDirection);
requireVolumeSize(volumeDisplacementDual);
requireParameterSize(parameterDualAction);
m_surface.buildSurfaceDisplacement(parameters, m_surfaceDisplacementWorkspace);
m_surface.applyJacobian(parameters, parameterDirection, m_surfaceDirectionWorkspace);
splitVolumeDual(volumeDisplacementDual);
applyExtensionTransposes();
m_interior.applyPullbackDerivative(
m_surfaceDisplacementWorkspace, m_surfaceDirectionWorkspace, m_interiorVolumeDualWorkspace,
m_interiorSurfacePullbackWorkspace
);
m_vacuum.applyPullbackDerivative(
m_surfaceDisplacementWorkspace, m_surfaceDirectionWorkspace, m_vacuumVolumeDualWorkspace,
m_vacuumSurfacePullbackWorkspace
);
addSurfaceFields(
m_interiorSurfacePullbackWorkspace, m_vacuumSurfacePullbackWorkspace, m_surfacePullbackWorkspace
);
m_surface.applyJacobianTranspose(parameters, m_surfacePullbackWorkspace, parameterDualAction);
m_surface.applyPullbackDerivative(
parameters, parameterDirection, m_surfaceDualWorkspace, m_parameterPullbackWorkspace
);
parameterDualAction += m_parameterPullbackWorkspace;
++m_actionStatistics.pullbackDerivativeApplications;
}
[[nodiscard]] DomainDeformationGeometryReport
inspectMappedGeometry(const mfem::Vector &volumeDisplacement) const {
requireCurrentDiscretization();
requireVolumeSize(volumeDisplacement);
m_volumeGridFunctionWorkspace->SetFromTrueDofs(volumeDisplacement);
mfem::Mesh *mesh = m_volumeDisplacementSpace->GetMesh();
double localMinimumDeterminant = std::numeric_limits<double>::infinity();
int localGeometryIsFinite = 1;
for (int element = 0; element < mesh->GetNE(); ++element) {
mfem::ElementTransformation *transformation = mesh->GetElementTransformation(element);
const mfem::FiniteElement *finiteElement = m_volumeDisplacementSpace->GetFE(element);
// Positivity is a pointwise geometry requirement, not an
// integration-accuracy requirement. A rule only slightly
// above the displacement order can miss a narrow negative
// region of the determinant even when a downstream physics
// rule samples it. The determinant of a d-dimensional
// degree-p deformation gradient can vary at substantially
// higher order, so inspect at a conservative d*p scale.
const int geometryInspectionOrder =
std::max(finiteElement->GetOrder() + 2, 2 * spatialDimension() * finiteElement->GetOrder());
const mfem::IntegrationRule &rule =
mfem::IntRules.Get(transformation->GetGeometryType(), geometryInspectionOrder);
for (int point = 0; point < rule.GetNPoints(); ++point) {
transformation->SetIntPoint(&rule.IntPoint(point));
mfem::DenseMatrix deformationGradient;
m_volumeGridFunctionWorkspace->GetVectorGradient(*transformation, deformationGradient);
for (int component = 0; component < spatialDimension(); ++component) {
deformationGradient(component, component) += 1.0;
}
const double determinant = deformationGradient.Det();
if (!std::isfinite(determinant)) {
localGeometryIsFinite = 0;
} else {
localMinimumDeterminant = std::min(localMinimumDeterminant, determinant);
}
}
}
double globalMinimumDeterminant = 0.0;
int globalGeometryIsFinite = 0;
MPI_Allreduce(
&localMinimumDeterminant, &globalMinimumDeterminant, 1, MPI_DOUBLE, MPI_MIN,
m_volumeDisplacementSpace->GetComm()
);
MPI_Allreduce(
&localGeometryIsFinite, &globalGeometryIsFinite, 1, MPI_INT, MPI_MIN,
m_volumeDisplacementSpace->GetComm()
);
if (globalGeometryIsFinite == 0) {
globalMinimumDeterminant = std::numeric_limits<double>::quiet_NaN();
}
++m_actionStatistics.geometryInspections;
return {.minimumJacobianDeterminant = globalMinimumDeterminant};
}
[[nodiscard]] DomainDeformationGeometryReport buildValidatedVolumeDisplacement(
const mfem::Vector &parameters,
mfem::Vector &volumeDisplacement,
const double determinantFloor = 0.0
) const {
if (!std::isfinite(determinantFloor) || determinantFloor < 0.0) {
throw std::invalid_argument("The mapped-geometry determinant floor must be finite and non-negative.");
}
buildVolumeDisplacement(parameters, volumeDisplacement);
const DomainDeformationGeometryReport report = inspectMappedGeometry(volumeDisplacement);
if (!report.isOrientationPreserving(determinantFloor)) {
throw std::domain_error("The prepared domain deformation inverts at least one volume element.");
}
return report;
}
private:
[[nodiscard]] static DomainDeformationDescriptor makeDescriptor(
const PreparedSurface &surface,
const PreparedInterior &interior,
const PreparedVacuum &vacuum
) noexcept {
const SurfaceDeformationDescriptor surfaceDescriptor = surface.descriptor();
const InteriorDeformationExtensionDescriptor interiorDescriptor = interior.descriptor();
const VacuumDeformationExtensionDescriptor vacuumDescriptor = vacuum.descriptor();
return {
.surfaceDeformation = surfaceDescriptor,
.stellarInteriorExtension = interiorDescriptor,
.vacuumExtension = vacuumDescriptor,
.linearOnReferenceGeometry = surfaceDescriptor.linearOnReferenceGeometry &&
interiorDescriptor.linearOnReferenceGeometry &&
vacuumDescriptor.linearOnReferenceGeometry,
.requiresAuxiliarySolve =
interiorDescriptor.requiresAuxiliarySolve || vacuumDescriptor.requiresAuxiliarySolve,
.hasExactDerivativeTranspose = surfaceDescriptor.hasExactDerivativeTranspose &&
interiorDescriptor.hasExactDerivativeTranspose &&
vacuumDescriptor.hasExactDerivativeTranspose,
.hasExactPullbackDerivative = surfaceDescriptor.hasExactPullbackDerivative &&
interiorDescriptor.hasExactPullbackDerivative &&
vacuumDescriptor.hasExactPullbackDerivative
};
}
void validateCompatibility(
mfem::ParFiniteElementSpace &surfaceScalarSpace,
mfem::ParFiniteElementSpace &volumeDisplacementSpace,
mfem::ParMesh &logicalReferenceMesh
) const {
const mfem::Mesh *physicalMesh = volumeDisplacementSpace.GetMesh();
if (!m_descriptor.isValid()) {
throw std::invalid_argument("Prepared domain deformation descriptors are incompatible.");
}
if (!m_descriptor.supportsExactNewtonLinearization()) {
throw std::invalid_argument("Prepared domain deformation requires exact transpose and pullback paths.");
}
if (physicalMesh == nullptr || surfaceScalarSpace.GetMesh() != physicalMesh) {
throw std::invalid_argument("Prepared domain deformation spaces must share one physical mesh.");
}
if (logicalReferenceMesh.GetNE() != physicalMesh->GetNE() ||
logicalReferenceMesh.GetNBE() != physicalMesh->GetNBE()) {
throw std::invalid_argument("Prepared domain deformation requires the paired logical reference mesh.");
}
if (m_surface.surfaceDisplacementSize() != m_interior.surfaceDisplacementSize() ||
m_surface.surfaceDisplacementSize() != m_vacuum.surfaceDisplacementSize()) {
throw std::invalid_argument("Prepared deformation factors have incompatible surface trace sizes.");
}
if (m_interior.interiorDisplacementSize() != m_vacuum.vacuumDisplacementSize() ||
m_interior.interiorDisplacementSize() != volumeDisplacementSpace.GetTrueVSize()) {
throw std::invalid_argument("Prepared deformation factors have incompatible volume vector sizes.");
}
if (m_interior.scalarTrueDofCount() != m_vacuum.scalarTrueDofCount() ||
volumeDisplacementSpace.GetTrueVSize() != spatialDimension() * m_interior.scalarTrueDofCount()) {
throw std::invalid_argument("Prepared deformation factors have incompatible scalar volume topology.");
}
if (volumeDisplacementSpace.GetOrdering() != mfem::Ordering::byNODES) {
throw std::invalid_argument("Prepared domain deformation requires MFEM byNODES volume ordering.");
}
}
void compileOwnership() {
m_volumeOwners.resize(static_cast<std::size_t>(scalarTrueDofCount()));
m_compositionReport.scalarTrueDofCount = scalarTrueDofCount();
for (int scalarTrueDof = 0; scalarTrueDof < scalarTrueDofCount(); ++scalarTrueDof) {
const bool hasStellarSupport = m_interior.hasStellarSupport(scalarTrueDof);
const bool hasVacuumSupport = m_vacuum.hasVacuumSupport(scalarTrueDof);
if (!hasStellarSupport && !hasVacuumSupport) {
throw std::invalid_argument("A volume displacement DOF has no deformation-extension owner.");
}
if (hasStellarSupport) {
m_volumeOwners[static_cast<std::size_t>(scalarTrueDof)] = VolumeDeformationOwner::StellarInterior;
++m_compositionReport.stellarInteriorOwnedScalarDofCount;
if (hasVacuumSupport) {
++m_compositionReport.sharedSurfaceScalarDofCount;
}
} else {
m_volumeOwners[static_cast<std::size_t>(scalarTrueDof)] = VolumeDeformationOwner::Vacuum;
++m_compositionReport.vacuumOwnedScalarDofCount;
}
}
}
[[nodiscard]] int volumeVectorDof(
const int scalarTrueDof,
const int component
) const noexcept {
return scalarTrueDof + component * scalarTrueDofCount();
}
void mergeVolumeFields(
const mfem::Vector &interiorVolume,
const mfem::Vector &vacuumVolume,
mfem::Vector &volume
) const noexcept {
for (int scalarTrueDof = 0; scalarTrueDof < scalarTrueDofCount(); ++scalarTrueDof) {
const mfem::Vector &source = volumeOwner(scalarTrueDof) == VolumeDeformationOwner::StellarInterior
? interiorVolume
: vacuumVolume;
for (int component = 0; component < spatialDimension(); ++component) {
const int vectorDof = volumeVectorDof(scalarTrueDof, component);
volume(vectorDof) = source(vectorDof);
}
}
}
void splitVolumeDual(const mfem::Vector &volumeDual) const noexcept {
m_interiorVolumeDualWorkspace = 0.0;
m_vacuumVolumeDualWorkspace = 0.0;
for (int scalarTrueDof = 0; scalarTrueDof < scalarTrueDofCount(); ++scalarTrueDof) {
mfem::Vector &destination = volumeOwner(scalarTrueDof) == VolumeDeformationOwner::StellarInterior
? m_interiorVolumeDualWorkspace
: m_vacuumVolumeDualWorkspace;
for (int component = 0; component < spatialDimension(); ++component) {
const int vectorDof = volumeVectorDof(scalarTrueDof, component);
destination(vectorDof) = volumeDual(vectorDof);
}
}
}
void applyExtensionTransposes() const {
m_interior.applyJacobianTranspose(
m_surfaceDisplacementWorkspace, m_interiorVolumeDualWorkspace, m_interiorSurfaceDualWorkspace
);
m_vacuum.applyJacobianTranspose(
m_surfaceDisplacementWorkspace, m_vacuumVolumeDualWorkspace, m_vacuumSurfaceDualWorkspace
);
addSurfaceFields(m_interiorSurfaceDualWorkspace, m_vacuumSurfaceDualWorkspace, m_surfaceDualWorkspace);
}
static void addSurfaceFields(
const mfem::Vector &interior,
const mfem::Vector &vacuum,
mfem::Vector &sum
) {
sum = interior;
sum += vacuum;
}
void requireCurrentDiscretization() const {
if (!matchesCurrentDiscretization()) {
throw std::logic_error("Prepared domain deformation discretization dependencies are stale.");
}
}
void requireParameterSize(const mfem::Vector &parameters) const {
if (parameters.Size() != parameterCount()) {
throw std::invalid_argument("Prepared domain deformation received an incompatible parameter vector.");
}
}
void requireVolumeSize(const mfem::Vector &volume) const {
if (volume.Size() != volumeDisplacementSize()) {
throw std::invalid_argument("Prepared domain deformation received an incompatible volume vector.");
}
}
void requireScalarTrueDof(const int scalarTrueDof) const {
if (scalarTrueDof < 0 || scalarTrueDof >= scalarTrueDofCount()) {
throw std::out_of_range("Scalar true DOF is outside the prepared domain deformation.");
}
}
PreparedSurface m_surface;
PreparedInterior m_interior;
PreparedVacuum m_vacuum;
mfem::ParFiniteElementSpace *m_volumeDisplacementSpace;
DomainDeformationDescriptor m_descriptor;
DomainDeformationCompositionReport m_compositionReport;
DomainDeformationDiscretizationDependencies m_discretizationDependencies;
std::vector<VolumeDeformationOwner> m_volumeOwners;
mutable PreparedDomainDeformationActionStatistics m_actionStatistics;
mutable mfem::Vector m_surfaceDisplacementWorkspace;
mutable mfem::Vector m_surfaceDirectionWorkspace;
mutable mfem::Vector m_interiorVolumeWorkspace;
mutable mfem::Vector m_vacuumVolumeWorkspace;
mutable mfem::Vector m_interiorVolumeDualWorkspace;
mutable mfem::Vector m_vacuumVolumeDualWorkspace;
mutable mfem::Vector m_interiorSurfaceDualWorkspace;
mutable mfem::Vector m_vacuumSurfaceDualWorkspace;
mutable mfem::Vector m_surfaceDualWorkspace;
mutable mfem::Vector m_interiorSurfacePullbackWorkspace;
mutable mfem::Vector m_vacuumSurfacePullbackWorkspace;
mutable mfem::Vector m_surfacePullbackWorkspace;
mutable mfem::Vector m_parameterPullbackWorkspace;
mutable std::unique_ptr<mfem::ParGridFunction> m_volumeGridFunctionWorkspace;
};
template <
PreparedSurfaceDeformationPrescription PreparedSurface,
PreparedInteriorDeformationExtension PreparedInterior,
PreparedVacuumDeformationExtension PreparedVacuum>
[[nodiscard]] auto composePreparedDomainDeformation(
PreparedSurface preparedSurface,
PreparedInterior preparedInterior,
PreparedVacuum preparedVacuum,
mfem::ParFiniteElementSpace &surfaceScalarSpace,
mfem::ParFiniteElementSpace &volumeDisplacementSpace,
mfem::ParMesh &logicalReferenceMesh
) {
return PreparedDomainDeformation<PreparedSurface, PreparedInterior, PreparedVacuum>{
std::move(preparedSurface), std::move(preparedInterior), std::move(preparedVacuum),
surfaceScalarSpace, volumeDisplacementSpace, logicalReferenceMesh
};
}
class PreparedDomainDeformationRuntime final {
public:
template <PreparedDomainDeformationOperator PreparedDeformation>
requires(!std::same_as<
std::remove_cvref_t<PreparedDeformation>,
PreparedDomainDeformationRuntime>)
explicit PreparedDomainDeformationRuntime(PreparedDeformation &&preparedDeformation)
: m_implementation(
std::make_unique<Implementation<std::remove_cvref_t<PreparedDeformation>>>(
std::forward<PreparedDeformation>(preparedDeformation)
)
) {
}
PreparedDomainDeformationRuntime(const PreparedDomainDeformationRuntime &) = delete;
PreparedDomainDeformationRuntime &operator=(const PreparedDomainDeformationRuntime &) = delete;
PreparedDomainDeformationRuntime(PreparedDomainDeformationRuntime &&) noexcept = default;
PreparedDomainDeformationRuntime &operator=(PreparedDomainDeformationRuntime &&) noexcept = default;
[[nodiscard]] DomainDeformationDescriptor descriptor() const noexcept {
return m_implementation->descriptor();
}
[[nodiscard]] int parameterCount() const noexcept {
return m_implementation->parameterCount();
}
[[nodiscard]] int surfaceDisplacementSize() const noexcept {
return m_implementation->surfaceDisplacementSize();
}
[[nodiscard]] int volumeDisplacementSize() const noexcept {
return m_implementation->volumeDisplacementSize();
}
[[nodiscard]] bool matchesCurrentDiscretization() const noexcept {
return m_implementation->matchesCurrentDiscretization();
}
[[nodiscard]] DomainDeformationCompositionReport compositionReport() const noexcept {
return m_implementation->compositionReport();
}
[[nodiscard]] DomainDeformationDiscretizationDependencies discretizationDependencies() const noexcept {
return m_implementation->discretizationDependencies();
}
[[nodiscard]] PreparedDomainDeformationActionStatistics actionStatistics() const noexcept {
return m_implementation->actionStatistics();
}
void buildVolumeDisplacement(
const mfem::Vector &parameters,
mfem::Vector &volumeDisplacement
) const {
m_implementation->buildVolumeDisplacement(parameters, volumeDisplacement);
}
void applyJacobian(
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
mfem::Vector &volumeDisplacementDirection
) const {
m_implementation->applyJacobian(parameters, parameterDirection, volumeDisplacementDirection);
}
void applyJacobianTranspose(
const mfem::Vector &parameters,
const mfem::Vector &volumeDisplacementDual,
mfem::Vector &parameterDual
) const {
m_implementation->applyJacobianTranspose(parameters, volumeDisplacementDual, parameterDual);
}
void applyPullbackDerivative(
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
const mfem::Vector &volumeDisplacementDual,
mfem::Vector &parameterDualAction
) const {
m_implementation->applyPullbackDerivative(
parameters, parameterDirection, volumeDisplacementDual, parameterDualAction
);
}
[[nodiscard]] DomainDeformationGeometryReport
inspectMappedGeometry(const mfem::Vector &volumeDisplacement) const {
return m_implementation->inspectMappedGeometry(volumeDisplacement);
}
[[nodiscard]] DomainDeformationGeometryReport buildValidatedVolumeDisplacement(
const mfem::Vector &parameters,
mfem::Vector &volumeDisplacement,
const double determinantFloor = 0.0
) const {
return m_implementation->buildValidatedVolumeDisplacement(parameters, volumeDisplacement, determinantFloor);
}
private:
class Interface {
public:
virtual ~Interface() = default;
[[nodiscard]] virtual DomainDeformationDescriptor descriptor() const noexcept = 0;
[[nodiscard]] virtual int parameterCount() const noexcept = 0;
[[nodiscard]] virtual int surfaceDisplacementSize() const noexcept = 0;
[[nodiscard]] virtual int volumeDisplacementSize() const noexcept = 0;
[[nodiscard]] virtual bool matchesCurrentDiscretization() const noexcept = 0;
[[nodiscard]] virtual DomainDeformationCompositionReport compositionReport() const noexcept = 0;
[[nodiscard]] virtual DomainDeformationDiscretizationDependencies
discretizationDependencies() const noexcept = 0;
[[nodiscard]] virtual PreparedDomainDeformationActionStatistics actionStatistics() const noexcept = 0;
virtual void buildVolumeDisplacement(
const mfem::Vector &,
mfem::Vector &
) const = 0;
virtual void applyJacobian(
const mfem::Vector &,
const mfem::Vector &,
mfem::Vector &
) const = 0;
virtual void applyJacobianTranspose(
const mfem::Vector &,
const mfem::Vector &,
mfem::Vector &
) const = 0;
virtual void applyPullbackDerivative(
const mfem::Vector &,
const mfem::Vector &,
const mfem::Vector &,
mfem::Vector &
) const = 0;
[[nodiscard]] virtual DomainDeformationGeometryReport inspectMappedGeometry(const mfem::Vector &) const = 0;
[[nodiscard]] virtual DomainDeformationGeometryReport buildValidatedVolumeDisplacement(
const mfem::Vector &,
mfem::Vector &,
double
) const = 0;
};
template <PreparedDomainDeformationOperator PreparedDeformation> class Implementation final : public Interface {
public:
explicit Implementation(PreparedDeformation preparedDeformation)
: m_preparedDeformation(std::move(preparedDeformation)) {
}
[[nodiscard]] DomainDeformationDescriptor descriptor() const noexcept override {
return m_preparedDeformation.descriptor();
}
[[nodiscard]] int parameterCount() const noexcept override {
return m_preparedDeformation.parameterCount();
}
[[nodiscard]] int surfaceDisplacementSize() const noexcept override {
return m_preparedDeformation.surfaceDisplacementSize();
}
[[nodiscard]] int volumeDisplacementSize() const noexcept override {
return m_preparedDeformation.volumeDisplacementSize();
}
[[nodiscard]] bool matchesCurrentDiscretization() const noexcept override {
return m_preparedDeformation.matchesCurrentDiscretization();
}
[[nodiscard]] DomainDeformationCompositionReport compositionReport() const noexcept override {
return m_preparedDeformation.compositionReport();
}
[[nodiscard]] DomainDeformationDiscretizationDependencies
discretizationDependencies() const noexcept override {
return m_preparedDeformation.discretizationDependencies();
}
[[nodiscard]] PreparedDomainDeformationActionStatistics actionStatistics() const noexcept override {
return m_preparedDeformation.actionStatistics();
}
void buildVolumeDisplacement(
const mfem::Vector &parameters,
mfem::Vector &volumeDisplacement
) const override {
m_preparedDeformation.buildVolumeDisplacement(parameters, volumeDisplacement);
}
void applyJacobian(
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
mfem::Vector &volumeDisplacementDirection
) const override {
m_preparedDeformation.applyJacobian(parameters, parameterDirection, volumeDisplacementDirection);
}
void applyJacobianTranspose(
const mfem::Vector &parameters,
const mfem::Vector &volumeDisplacementDual,
mfem::Vector &parameterDual
) const override {
m_preparedDeformation.applyJacobianTranspose(parameters, volumeDisplacementDual, parameterDual);
}
void applyPullbackDerivative(
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
const mfem::Vector &volumeDisplacementDual,
mfem::Vector &parameterDualAction
) const override {
m_preparedDeformation.applyPullbackDerivative(
parameters, parameterDirection, volumeDisplacementDual, parameterDualAction
);
}
[[nodiscard]] DomainDeformationGeometryReport
inspectMappedGeometry(const mfem::Vector &volumeDisplacement) const override {
return m_preparedDeformation.inspectMappedGeometry(volumeDisplacement);
}
[[nodiscard]] DomainDeformationGeometryReport buildValidatedVolumeDisplacement(
const mfem::Vector &parameters,
mfem::Vector &volumeDisplacement,
const double determinantFloor
) const override {
return m_preparedDeformation.buildValidatedVolumeDisplacement(
parameters, volumeDisplacement, determinantFloor
);
}
private:
PreparedDeformation m_preparedDeformation;
};
std::unique_ptr<Interface> m_implementation;
};
template <
utils::domain::IsSchema SchemaT = utils::domain::CoreEnvelopeVacuumDomainSchema,
SurfaceDeformationPrescription SurfacePrescription,
InteriorDeformationExtension InteriorExtension,
VacuumDeformationExtension VacuumExtension>
requires SurfaceDeformationCompilable<
SurfacePrescription,
SurfaceDeformationCompilationContext> &&
InteriorDeformationExtensionCompilable<
InteriorExtension,
RadialDeformationExtensionCompilationContext> &&
VacuumDeformationExtensionCompilable<
VacuumExtension,
RadialDeformationExtensionCompilationContext>
[[nodiscard]] auto compileDomainDeformation(
const SurfacePrescription &surfacePrescription,
const InteriorExtension &interiorExtension,
const VacuumExtension &vacuumExtension,
fem::FEM &finiteElementModel
) {
if (!finiteElementModel.okay()) {
throw std::invalid_argument("Domain deformation compilation requires a complete finite-element model.");
}
const field::ScalarBoundaryDofMap surfaceDofMap =
field::make_stellar_surface_scalar_dof_map<SchemaT>(*finiteElementModel.surfaceDeformationFes);
const SurfaceDeformationCompilationContext surfaceContext{
*finiteElementModel.surfaceDeformationFes, surfaceDofMap
};
auto preparedSurface = compileSurfaceDeformationPrescription(surfacePrescription, surfaceContext);
const RadialDeformationExtensionCompilationContext extensionContext =
makeRadialDeformationExtensionCompilationContext<SchemaT>(
*finiteElementModel.surfaceDeformationFes, *finiteElementModel.displacementFes,
*finiteElementModel.logicalReferenceMesh
);
auto preparedInterior = compileInteriorDeformationExtension(interiorExtension, extensionContext);
auto preparedVacuum = compileVacuumDeformationExtension(vacuumExtension, extensionContext);
return composePreparedDomainDeformation(
std::move(preparedSurface), std::move(preparedInterior), std::move(preparedVacuum),
*finiteElementModel.surfaceDeformationFes, *finiteElementModel.displacementFes,
*finiteElementModel.logicalReferenceMesh
);
}
} // namespace mean_field::deformation

View File

@@ -0,0 +1,63 @@
module;
#include <concepts>
#include <type_traits>
#include <mfem.hpp>
export module mean_field:deformation.interior_extension;
export import :deformation.descriptors;
export namespace mean_field::deformation {
template <typename Candidate>
concept PreparedInteriorDeformationExtension = requires(
const std::remove_cvref_t<Candidate> &preparedExtension,
const mfem::Vector &surfaceDisplacement,
const mfem::Vector &surfaceDisplacementDirection,
const mfem::Vector &interiorDisplacementDual,
mfem::Vector &interiorDisplacement,
mfem::Vector &surfaceDisplacementDual
) {
{ preparedExtension.descriptor() } noexcept -> std::same_as<InteriorDeformationExtensionDescriptor>;
{ preparedExtension.surfaceDisplacementSize() } noexcept -> std::same_as<int>;
{ preparedExtension.interiorDisplacementSize() } noexcept -> std::same_as<int>;
{ preparedExtension.scalarTrueDofCount() } noexcept -> std::same_as<int>;
{ preparedExtension.hasStellarSupport(0) } -> std::same_as<bool>;
{
preparedExtension.buildInteriorDisplacement(surfaceDisplacement, interiorDisplacement)
} -> std::same_as<void>;
{
preparedExtension.applyJacobian(surfaceDisplacement, surfaceDisplacementDirection, interiorDisplacement)
} -> std::same_as<void>;
{
preparedExtension.applyJacobianTranspose(
surfaceDisplacement, interiorDisplacementDual, surfaceDisplacementDual
)
} -> std::same_as<void>;
{
preparedExtension.applyPullbackDerivative(
surfaceDisplacement, surfaceDisplacementDirection, interiorDisplacementDual, surfaceDisplacementDual
)
} -> std::same_as<void>;
};
template <typename Candidate>
concept InteriorDeformationExtension = requires(const std::remove_cvref_t<Candidate> &extension) {
typename std::remove_cvref_t<Candidate>::PreparedType;
requires PreparedInteriorDeformationExtension<typename std::remove_cvref_t<Candidate>::PreparedType>;
{ extension.descriptor() } noexcept -> std::same_as<InteriorDeformationExtensionDescriptor>;
{ extension.validate() } -> std::same_as<void>;
};
template <typename Extension, typename CompilationContext>
concept InteriorDeformationExtensionCompilable =
InteriorDeformationExtension<Extension> && requires(
const std::remove_cvref_t<Extension> &extension,
const std::remove_cvref_t<CompilationContext> &context
) {
{
compileInteriorDeformationExtension(extension, context)
} -> std::same_as<typename std::remove_cvref_t<Extension>::PreparedType>;
};
} // namespace mean_field::deformation

View File

@@ -0,0 +1,138 @@
module;
#include <mfem.hpp>
export module mean_field:deformation.nodal_radial_surface;
export import :deformation.surface_prescription;
export import :field.mfem;
export namespace mean_field::deformation {
class PreparedNodalRadialSurface;
class SurfaceDeformationCompilationContext final {
public:
SurfaceDeformationCompilationContext(
mfem::ParFiniteElementSpace &scalarFiniteElementSpace,
field::ScalarBoundaryDofMap surfaceDofMap
);
[[nodiscard]] mfem::ParFiniteElementSpace &scalarFiniteElementSpace() const noexcept;
[[nodiscard]] const field::ScalarBoundaryDofMap &surfaceDofMap() const noexcept;
private:
mfem::ParFiniteElementSpace *m_scalarFiniteElementSpace;
field::ScalarBoundaryDofMap m_surfaceDofMap;
};
class NodalRadialSurface final {
public:
using PreparedType = PreparedNodalRadialSurface;
explicit NodalRadialSurface(mfem::Vector referenceCenter);
[[nodiscard]] const mfem::Vector &referenceCenter() const noexcept;
[[nodiscard]] SurfaceDeformationDescriptor descriptor() const noexcept;
void validate() const;
private:
mfem::Vector m_referenceCenter;
};
class PreparedNodalRadialSurface final {
public:
[[nodiscard]] SurfaceDeformationDescriptor descriptor() const noexcept;
[[nodiscard]] int parameterCount() const noexcept;
[[nodiscard]] long long globalParameterCount() const noexcept;
[[nodiscard]] long long globalParameterOffset() const noexcept;
[[nodiscard]] int spatialDimension() const noexcept;
[[nodiscard]] int surfaceDisplacementSize() const noexcept;
[[nodiscard]] long long globalSurfaceDisplacementSize() const noexcept;
[[nodiscard]] long long globalSurfaceDisplacementOffset() const noexcept;
[[nodiscard]] int surfaceDisplacementDof(
int parameterDof,
int component
) const;
[[nodiscard]] double radialDirection(
int parameterDof,
int component
) const;
[[nodiscard]] double referenceRadius(int parameterDof) const;
[[nodiscard]] const mfem::Vector &referenceCenter() const noexcept;
[[nodiscard]] const field::ScalarBoundaryDofMap &surfaceDofMap() const noexcept;
void buildSurfaceDisplacement(
const mfem::Vector &parameters,
mfem::Vector &surfaceDisplacement
) const;
void applyJacobian(
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
mfem::Vector &surfaceDisplacementDirection
) const;
void applyJacobianTranspose(
const mfem::Vector &parameters,
const mfem::Vector &surfaceDisplacementDual,
mfem::Vector &parameterDual
) const;
void applyPullbackDerivative(
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
const mfem::Vector &surfaceDisplacementDual,
mfem::Vector &parameterDualAction
) const;
private:
friend PreparedNodalRadialSurface compileSurfaceDeformationPrescription(
const NodalRadialSurface &prescription,
const SurfaceDeformationCompilationContext &context
);
PreparedNodalRadialSurface(
SurfaceDeformationDescriptor descriptor,
mfem::Vector referenceCenter,
field::ScalarBoundaryDofMap surfaceDofMap,
mfem::Vector radialDirections,
mfem::Vector referenceRadii
);
void requireParameterSize(const mfem::Vector &parameters) const;
void requireSurfaceDisplacementSize(const mfem::Vector &surfaceDisplacement) const;
SurfaceDeformationDescriptor m_descriptor;
mfem::Vector m_referenceCenter;
field::ScalarBoundaryDofMap m_surfaceDofMap;
mfem::Vector m_radialDirections;
mfem::Vector m_referenceRadii;
};
[[nodiscard]] PreparedNodalRadialSurface compileSurfaceDeformationPrescription(
const NodalRadialSurface &prescription,
const SurfaceDeformationCompilationContext &context
);
static_assert(SurfaceDeformationPrescription<NodalRadialSurface>);
static_assert(PreparedSurfaceDeformationPrescription<PreparedNodalRadialSurface>);
static_assert(SurfaceDeformationCompilable<
NodalRadialSurface,
SurfaceDeformationCompilationContext>);
} // namespace mean_field::deformation

View File

@@ -0,0 +1,302 @@
module;
#include <utility>
#include <vector>
#include <mfem.hpp>
#include <mpi.h>
export module mean_field:deformation.radial_extensions;
export import :deformation.interior_extension;
export import :deformation.vacuum_extension;
export import :field.mfem;
export import :utils.domain;
export namespace mean_field::deformation {
class RadialDeformationExtensionCompilationContext final {
public:
RadialDeformationExtensionCompilationContext(
mfem::ParFiniteElementSpace &scalarFiniteElementSpace,
mfem::ParFiniteElementSpace &vectorFiniteElementSpace,
mfem::ParMesh &logicalReferenceMesh,
field::ScalarBoundaryDofMap stellarSurfaceDofMap,
field::ScalarBoundaryDofMap infinitySurfaceDofMap,
mfem::Array<int> stellarMaterialMarker,
mfem::Array<int> vacuumMaterialMarker,
int stellarSurfaceBoundaryAttribute,
int infinitySurfaceBoundaryAttribute
);
[[nodiscard]] int spatialDimension() const noexcept;
[[nodiscard]] int surfaceDisplacementSize() const noexcept;
[[nodiscard]] int volumeDisplacementSize() const noexcept;
[[nodiscard]] int scalarTrueDofCount() const noexcept;
[[nodiscard]] double logicalRadius(int scalarTrueDof) const;
[[nodiscard]] double stellarSurfaceLogicalRadius() const noexcept;
[[nodiscard]] double infinitySurfaceLogicalRadius() const noexcept;
[[nodiscard]] int surfaceInterpolationEntryCount(int scalarTrueDof) const;
[[nodiscard]] int surfaceGlobalCoordinate(
int scalarTrueDof,
int interpolationEntry
) const;
[[nodiscard]] double surfaceInterpolationWeight(
int scalarTrueDof,
int interpolationEntry
) const;
private:
friend class PreparedPowerLawRadialInteriorExtension;
friend class PreparedFixedInfinityRadialVacuumExtension;
int m_spatialDimension{0};
int m_scalarTrueDofCount{0};
int m_volumeDisplacementSize{0};
int m_surfaceDisplacementSize{0};
int m_globalSurfaceDisplacementSize{0};
int m_globalSurfaceDisplacementOffset{0};
MPI_Comm m_communicator{MPI_COMM_NULL};
mfem::Array<int> m_stellarSupport;
mfem::Array<int> m_vacuumSupport;
mfem::Vector m_logicalRadius;
double m_stellarSurfaceLogicalRadius{0.0};
double m_infinitySurfaceLogicalRadius{0.0};
std::vector<int> m_surfaceInterpolationRowOffsets;
std::vector<int> m_surfaceInterpolationGlobalCoordinates;
std::vector<double> m_surfaceInterpolationWeights;
std::vector<int> m_surfaceDisplacementCounts;
std::vector<int> m_surfaceDisplacementOffsets;
};
template <utils::domain::IsSchema SchemaT = utils::domain::CoreEnvelopeVacuumDomainSchema>
requires(
SchemaT::template contains_domain<utils::domain::Stellar>() &&
SchemaT::template contains_domain<utils::domain::Vacuum>() &&
SchemaT::template contains_boundary<utils::domain::StellarSurface>() &&
SchemaT::template contains_boundary<utils::domain::InfinitySurface>()
)
[[nodiscard]] RadialDeformationExtensionCompilationContext makeRadialDeformationExtensionCompilationContext(
mfem::ParFiniteElementSpace &scalarFiniteElementSpace,
mfem::ParFiniteElementSpace &vectorFiniteElementSpace,
mfem::ParMesh &logicalReferenceMesh
) {
const mfem::Mesh *mesh = scalarFiniteElementSpace.GetMesh();
MFEM_VERIFY(mesh != nullptr, "Radial deformation extension compilation requires an MFEM mesh.");
return RadialDeformationExtensionCompilationContext(
scalarFiniteElementSpace, vectorFiniteElementSpace, logicalReferenceMesh,
field::make_scalar_boundary_dof_map<utils::domain::StellarSurface, SchemaT>(scalarFiniteElementSpace),
field::make_scalar_boundary_dof_map<utils::domain::InfinitySurface, SchemaT>(scalarFiniteElementSpace),
utils::domain::make_attribute_marker<utils::domain::Stellar, SchemaT>(*mesh),
utils::domain::make_attribute_marker<utils::domain::Vacuum, SchemaT>(*mesh),
SchemaT::template boundary_attribute<utils::domain::StellarSurface>(),
SchemaT::template boundary_attribute<utils::domain::InfinitySurface>()
);
}
class PreparedPowerLawRadialInteriorExtension;
class PowerLawRadialInteriorExtension final {
public:
using PreparedType = PreparedPowerLawRadialInteriorExtension;
explicit PowerLawRadialInteriorExtension(double radialPower = 2.0);
[[nodiscard]] double radialPower() const noexcept;
[[nodiscard]] InteriorDeformationExtensionDescriptor descriptor() const noexcept;
void validate() const;
private:
double m_radialPower;
};
class PreparedPowerLawRadialInteriorExtension final {
public:
[[nodiscard]] InteriorDeformationExtensionDescriptor descriptor() const noexcept;
[[nodiscard]] int surfaceDisplacementSize() const noexcept;
[[nodiscard]] int interiorDisplacementSize() const noexcept;
[[nodiscard]] int scalarTrueDofCount() const noexcept;
[[nodiscard]] double radialPower() const noexcept;
[[nodiscard]] bool hasStellarSupport(int scalarTrueDof) const;
[[nodiscard]] double radialWeight(int scalarTrueDof) const;
[[nodiscard]] int surfaceInterpolationEntryCount(int scalarTrueDof) const;
[[nodiscard]] int surfaceGlobalCoordinate(
int scalarTrueDof,
int interpolationEntry
) const;
[[nodiscard]] double surfaceInterpolationWeight(
int scalarTrueDof,
int interpolationEntry
) const;
void buildInteriorDisplacement(
const mfem::Vector &surfaceDisplacement,
mfem::Vector &interiorDisplacement
) const;
void applyJacobian(
const mfem::Vector &surfaceDisplacement,
const mfem::Vector &surfaceDisplacementDirection,
mfem::Vector &interiorDisplacementDirection
) const;
void applyJacobianTranspose(
const mfem::Vector &surfaceDisplacement,
const mfem::Vector &interiorDisplacementDual,
mfem::Vector &surfaceDisplacementDual
) const;
void applyPullbackDerivative(
const mfem::Vector &surfaceDisplacement,
const mfem::Vector &surfaceDisplacementDirection,
const mfem::Vector &interiorDisplacementDual,
mfem::Vector &surfaceDisplacementDualAction
) const;
private:
friend PreparedPowerLawRadialInteriorExtension compileInteriorDeformationExtension(
const PowerLawRadialInteriorExtension &extension,
const RadialDeformationExtensionCompilationContext &context
);
PreparedPowerLawRadialInteriorExtension(
const PowerLawRadialInteriorExtension &extension,
const RadialDeformationExtensionCompilationContext &context
);
void requireSurfaceSize(const mfem::Vector &surfaceDisplacement) const;
void requireInteriorSize(const mfem::Vector &interiorDisplacement) const;
void applyForward(
const mfem::Vector &surfaceDisplacement,
mfem::Vector &interiorDisplacement
) const;
void applyTranspose(
const mfem::Vector &interiorDisplacementDual,
mfem::Vector &surfaceDisplacementDual
) const;
InteriorDeformationExtensionDescriptor m_descriptor;
double m_radialPower{2.0};
int m_surfaceDisplacementSize{0};
int m_interiorDisplacementSize{0};
int m_spatialDimension{0};
int m_globalSurfaceDisplacementSize{0};
int m_globalSurfaceDisplacementOffset{0};
MPI_Comm m_communicator{MPI_COMM_NULL};
mfem::Array<int> m_stellarSupport;
mfem::Vector m_radialWeights;
std::vector<int> m_surfaceInterpolationRowOffsets;
std::vector<int> m_surfaceInterpolationGlobalCoordinates;
std::vector<double> m_surfaceInterpolationWeights;
std::vector<int> m_surfaceDisplacementCounts;
std::vector<int> m_surfaceDisplacementOffsets;
mutable mfem::Vector m_globalSurfaceDisplacementWorkspace;
mutable mfem::Vector m_localGlobalSurfaceDualWorkspace;
mutable mfem::Vector m_globalSurfaceDualWorkspace;
};
[[nodiscard]] PreparedPowerLawRadialInteriorExtension compileInteriorDeformationExtension(
const PowerLawRadialInteriorExtension &extension,
const RadialDeformationExtensionCompilationContext &context
);
class PreparedFixedInfinityRadialVacuumExtension;
class FixedInfinityRadialVacuumExtension final {
public:
using PreparedType = PreparedFixedInfinityRadialVacuumExtension;
[[nodiscard]] VacuumDeformationExtensionDescriptor descriptor() const noexcept;
void validate() const;
};
class PreparedFixedInfinityRadialVacuumExtension final {
public:
[[nodiscard]] VacuumDeformationExtensionDescriptor descriptor() const noexcept;
[[nodiscard]] int surfaceDisplacementSize() const noexcept;
[[nodiscard]] int vacuumDisplacementSize() const noexcept;
[[nodiscard]] int scalarTrueDofCount() const noexcept;
[[nodiscard]] bool hasVacuumSupport(int scalarTrueDof) const;
[[nodiscard]] double radialWeight(int scalarTrueDof) const;
[[nodiscard]] int surfaceInterpolationEntryCount(int scalarTrueDof) const;
[[nodiscard]] int surfaceGlobalCoordinate(
int scalarTrueDof,
int interpolationEntry
) const;
[[nodiscard]] double surfaceInterpolationWeight(
int scalarTrueDof,
int interpolationEntry
) const;
void buildVacuumDisplacement(
const mfem::Vector &surfaceDisplacement,
mfem::Vector &vacuumDisplacement
) const;
void applyJacobian(
const mfem::Vector &surfaceDisplacement,
const mfem::Vector &surfaceDisplacementDirection,
mfem::Vector &vacuumDisplacementDirection
) const;
void applyJacobianTranspose(
const mfem::Vector &surfaceDisplacement,
const mfem::Vector &vacuumDisplacementDual,
mfem::Vector &surfaceDisplacementDual
) const;
void applyPullbackDerivative(
const mfem::Vector &surfaceDisplacement,
const mfem::Vector &surfaceDisplacementDirection,
const mfem::Vector &vacuumDisplacementDual,
mfem::Vector &surfaceDisplacementDualAction
) const;
private:
friend PreparedFixedInfinityRadialVacuumExtension compileVacuumDeformationExtension(
const FixedInfinityRadialVacuumExtension &extension,
const RadialDeformationExtensionCompilationContext &context
);
PreparedFixedInfinityRadialVacuumExtension(
const FixedInfinityRadialVacuumExtension &extension,
const RadialDeformationExtensionCompilationContext &context
);
void requireSurfaceSize(const mfem::Vector &surfaceDisplacement) const;
void requireVacuumSize(const mfem::Vector &vacuumDisplacement) const;
void applyForward(
const mfem::Vector &surfaceDisplacement,
mfem::Vector &vacuumDisplacement
) const;
void applyTranspose(
const mfem::Vector &vacuumDisplacementDual,
mfem::Vector &surfaceDisplacementDual
) const;
VacuumDeformationExtensionDescriptor m_descriptor;
int m_surfaceDisplacementSize{0};
int m_vacuumDisplacementSize{0};
int m_spatialDimension{0};
int m_globalSurfaceDisplacementSize{0};
int m_globalSurfaceDisplacementOffset{0};
MPI_Comm m_communicator{MPI_COMM_NULL};
mfem::Array<int> m_vacuumSupport;
mfem::Vector m_radialWeights;
std::vector<int> m_surfaceInterpolationRowOffsets;
std::vector<int> m_surfaceInterpolationGlobalCoordinates;
std::vector<double> m_surfaceInterpolationWeights;
std::vector<int> m_surfaceDisplacementCounts;
std::vector<int> m_surfaceDisplacementOffsets;
mutable mfem::Vector m_globalSurfaceDisplacementWorkspace;
mutable mfem::Vector m_localGlobalSurfaceDualWorkspace;
mutable mfem::Vector m_globalSurfaceDualWorkspace;
};
[[nodiscard]] PreparedFixedInfinityRadialVacuumExtension compileVacuumDeformationExtension(
const FixedInfinityRadialVacuumExtension &extension,
const RadialDeformationExtensionCompilationContext &context
);
static_assert(InteriorDeformationExtension<PowerLawRadialInteriorExtension>);
static_assert(PreparedInteriorDeformationExtension<PreparedPowerLawRadialInteriorExtension>);
static_assert(InteriorDeformationExtensionCompilable<
PowerLawRadialInteriorExtension,
RadialDeformationExtensionCompilationContext>);
static_assert(VacuumDeformationExtension<FixedInfinityRadialVacuumExtension>);
static_assert(PreparedVacuumDeformationExtension<PreparedFixedInfinityRadialVacuumExtension>);
static_assert(VacuumDeformationExtensionCompilable<
FixedInfinityRadialVacuumExtension,
RadialDeformationExtensionCompilationContext>);
} // namespace mean_field::deformation

View File

@@ -0,0 +1,57 @@
module;
#include <concepts>
#include <type_traits>
#include <mfem.hpp>
export module mean_field:deformation.surface_prescription;
export import :deformation.descriptors;
export namespace mean_field::deformation {
template <typename Candidate>
concept PreparedSurfaceDeformationPrescription = requires(
const std::remove_cvref_t<Candidate> &preparedPrescription,
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
const mfem::Vector &surfaceDisplacementDual,
mfem::Vector &surfaceDisplacement,
mfem::Vector &parameterDual
) {
{ preparedPrescription.descriptor() } noexcept -> std::same_as<SurfaceDeformationDescriptor>;
{ preparedPrescription.parameterCount() } noexcept -> std::same_as<int>;
{ preparedPrescription.surfaceDisplacementSize() } noexcept -> std::same_as<int>;
{ preparedPrescription.buildSurfaceDisplacement(parameters, surfaceDisplacement) } -> std::same_as<void>;
{
preparedPrescription.applyJacobian(parameters, parameterDirection, surfaceDisplacement)
} -> std::same_as<void>;
{
preparedPrescription.applyJacobianTranspose(parameters, surfaceDisplacementDual, parameterDual)
} -> std::same_as<void>;
{
preparedPrescription.applyPullbackDerivative(
parameters, parameterDirection, surfaceDisplacementDual, parameterDual
)
} -> std::same_as<void>;
};
template <typename Candidate>
concept SurfaceDeformationPrescription = requires(const std::remove_cvref_t<Candidate> &prescription) {
typename std::remove_cvref_t<Candidate>::PreparedType;
requires PreparedSurfaceDeformationPrescription<typename std::remove_cvref_t<Candidate>::PreparedType>;
{ prescription.descriptor() } noexcept -> std::same_as<SurfaceDeformationDescriptor>;
{ prescription.validate() } -> std::same_as<void>;
};
template <typename Prescription, typename CompilationContext>
concept SurfaceDeformationCompilable =
SurfaceDeformationPrescription<Prescription> && requires(
const std::remove_cvref_t<Prescription> &prescription,
const std::remove_cvref_t<CompilationContext> &context
) {
{
compileSurfaceDeformationPrescription(prescription, context)
} -> std::same_as<typename std::remove_cvref_t<Prescription>::PreparedType>;
};
} // namespace mean_field::deformation

View File

@@ -0,0 +1,61 @@
module;
#include <concepts>
#include <type_traits>
#include <mfem.hpp>
export module mean_field:deformation.vacuum_extension;
export import :deformation.descriptors;
export namespace mean_field::deformation {
template <typename Candidate>
concept PreparedVacuumDeformationExtension = requires(
const std::remove_cvref_t<Candidate> &preparedExtension,
const mfem::Vector &surfaceDisplacement,
const mfem::Vector &surfaceDisplacementDirection,
const mfem::Vector &vacuumDisplacementDual,
mfem::Vector &vacuumDisplacement,
mfem::Vector &surfaceDisplacementDual
) {
{ preparedExtension.descriptor() } noexcept -> std::same_as<VacuumDeformationExtensionDescriptor>;
{ preparedExtension.surfaceDisplacementSize() } noexcept -> std::same_as<int>;
{ preparedExtension.vacuumDisplacementSize() } noexcept -> std::same_as<int>;
{ preparedExtension.scalarTrueDofCount() } noexcept -> std::same_as<int>;
{ preparedExtension.hasVacuumSupport(0) } -> std::same_as<bool>;
{ preparedExtension.buildVacuumDisplacement(surfaceDisplacement, vacuumDisplacement) } -> std::same_as<void>;
{
preparedExtension.applyJacobian(surfaceDisplacement, surfaceDisplacementDirection, vacuumDisplacement)
} -> std::same_as<void>;
{
preparedExtension.applyJacobianTranspose(
surfaceDisplacement, vacuumDisplacementDual, surfaceDisplacementDual
)
} -> std::same_as<void>;
{
preparedExtension.applyPullbackDerivative(
surfaceDisplacement, surfaceDisplacementDirection, vacuumDisplacementDual, surfaceDisplacementDual
)
} -> std::same_as<void>;
};
template <typename Candidate>
concept VacuumDeformationExtension = requires(const std::remove_cvref_t<Candidate> &extension) {
typename std::remove_cvref_t<Candidate>::PreparedType;
requires PreparedVacuumDeformationExtension<typename std::remove_cvref_t<Candidate>::PreparedType>;
{ extension.descriptor() } noexcept -> std::same_as<VacuumDeformationExtensionDescriptor>;
{ extension.validate() } -> std::same_as<void>;
};
template <typename Extension, typename CompilationContext>
concept VacuumDeformationExtensionCompilable =
VacuumDeformationExtension<Extension> && requires(
const std::remove_cvref_t<Extension> &extension,
const std::remove_cvref_t<CompilationContext> &context
) {
{
compileVacuumDeformationExtension(extension, context)
} -> std::same_as<typename std::remove_cvref_t<Extension>::PreparedType>;
};
} // namespace mean_field::deformation

View File

@@ -28,6 +28,7 @@ export namespace mean_field::fem {
stroid::StroidMesh smesh;
std::unique_ptr<mfem::ParMesh> mesh;
std::unique_ptr<mfem::ParMesh> logicalReferenceMesh;
// =====================================================================
// Compile-time field descriptors
@@ -63,6 +64,14 @@ export namespace mean_field::fem {
std::unique_ptr<mfem::ParGridFunction> displacement;
/*
* Scalar companion of the vector displacement space. Only its
* StellarSurface true DOFs become surface-deformation coordinates.
* Sharing displacementFec guarantees identical scalar basis
* functions without duplicating the finite-element collection.
*/
std::unique_ptr<mfem::ParFiniteElementSpace> surfaceDeformationFes;
// =====================================================================
// Density field
// =====================================================================
@@ -115,12 +124,13 @@ export namespace mean_field::fem {
// =====================================================================
[[nodiscard]] bool okay() const {
return mesh != nullptr &&
return mesh != nullptr && logicalReferenceMesh != nullptr &&
gravityPotentialFec != nullptr && gravityPotentialFes != nullptr && gravityFluxFec != nullptr &&
gravityFluxFes != nullptr &&
displacementFec != nullptr && displacementFes != nullptr && displacement != nullptr &&
surfaceDeformationFes != nullptr &&
densityFec != nullptr && densityFes != nullptr &&

View File

@@ -5,6 +5,7 @@ module;
#include <concepts>
#include <cstddef>
#include <memory>
#include <optional>
#include <stdexcept>
#include <utility>
@@ -889,6 +890,198 @@ export namespace mean_field::field {
mfem::Array<int> m_trueToReduced;
};
/*
* Canonical coordinates on a scalar finite-element boundary.
*
* Unlike FieldBoundaryDofMap, this is not a row selection inside an
* existing physical field. It defines an independent dense coordinate
* vector whose entries are the locally owned scalar true DOFs on a
* semantic boundary.
*
* Local coordinates follow increasing MFEM true-DOF order. Global
* coordinates use the distributed-vector convention: ranks are ordered by
* communicator rank and each rank contributes its locally sorted block.
*/
class ScalarBoundaryDofMap final {
public:
ScalarBoundaryDofMap() = default;
ScalarBoundaryDofMap(
const int volumeTrueDofSize,
const mfem::Array<int> &boundaryTrueDofs,
const long long globalOffset,
const long long globalSize
)
: m_boundaryDofs(
volumeTrueDofSize,
boundaryTrueDofs
),
m_globalOffset(globalOffset),
m_globalSize(globalSize) {
if (m_globalOffset < 0) {
throw std::invalid_argument("ScalarBoundaryDofMap requires a non-negative global offset.");
}
if (m_globalSize < 0) {
throw std::invalid_argument("ScalarBoundaryDofMap requires a non-negative global size.");
}
if (m_globalOffset + local_size() > m_globalSize) {
throw std::invalid_argument(
"ScalarBoundaryDofMap local coordinates lie outside the global boundary coordinate vector."
);
}
}
[[nodiscard]] int volume_true_dof_size() const noexcept {
return m_boundaryDofs.full_size();
}
[[nodiscard]] int local_size() const noexcept {
return m_boundaryDofs.reduced_size();
}
[[nodiscard]] long long global_size() const noexcept {
return m_globalSize;
}
[[nodiscard]] long long global_offset() const noexcept {
return m_globalOffset;
}
[[nodiscard]] bool empty() const noexcept {
return local_size() == 0;
}
[[nodiscard]] const mfem::Array<int> &boundary_true_dofs() const noexcept {
return m_boundaryDofs.reduced_to_true();
}
[[nodiscard]] int volume_true_dof(const int localBoundaryDof) const {
return m_boundaryDofs.true_dof(localBoundaryDof);
}
[[nodiscard]] std::optional<int> local_boundary_dof(const int volumeTrueDof) const {
return m_boundaryDofs.reduced_dof(volumeTrueDof);
}
[[nodiscard]] bool contains_volume_true_dof(const int volumeTrueDof) const {
return m_boundaryDofs.contains_true_dof(volumeTrueDof);
}
[[nodiscard]] long long global_boundary_dof(const int localBoundaryDof) const {
if (localBoundaryDof < 0 || localBoundaryDof >= local_size()) {
throw std::out_of_range("Local boundary DOF is outside ScalarBoundaryDofMap.");
}
return m_globalOffset + localBoundaryDof;
}
void gather(
const mfem::Vector &volumeTrueValues,
mfem::Vector &boundaryValues
) const {
m_boundaryDofs.gather(volumeTrueValues, boundaryValues);
}
[[nodiscard]] mfem::Vector gather(const mfem::Vector &volumeTrueValues) const {
return m_boundaryDofs.gather(volumeTrueValues);
}
void scatter(
const mfem::Vector &boundaryValues,
mfem::Vector &volumeTrueValues
) const {
m_boundaryDofs.scatter(boundaryValues, volumeTrueValues);
}
[[nodiscard]] mfem::Vector scatter(const mfem::Vector &boundaryValues) const {
return m_boundaryDofs.scatter(boundaryValues);
}
private:
FieldDofMap m_boundaryDofs;
long long m_globalOffset{0};
long long m_globalSize{0};
};
template <
utils::domain::IsBoundary BoundaryT,
utils::domain::IsSchema SchemaT>
requires(SchemaT::template contains_boundary<BoundaryT>())
[[nodiscard]] ScalarBoundaryDofMap
make_scalar_boundary_dof_map(const mfem::ParFiniteElementSpace &finiteElementSpace) {
MFEM_VERIFY(
!finiteElementSpace.Nonconforming(),
"Scalar boundary true-DOF resolution currently requires a conforming mfem::ParFiniteElementSpace."
);
MFEM_VERIFY(finiteElementSpace.GetVDim() == 1, "ScalarBoundaryDofMap requires a scalar finite-element space.");
const mfem::Mesh *mesh = finiteElementSpace.GetMesh();
MFEM_VERIFY(mesh != nullptr, "Scalar boundary DOF resolution requires an MFEM mesh.");
mfem::Array<int> boundaryVDofMarker(finiteElementSpace.GetVSize());
boundaryVDofMarker = 0;
mfem::Array<int> boundaryElementVDofs;
for (int boundaryElement = 0; boundaryElement < mesh->GetNBE(); ++boundaryElement) {
if (!SchemaT::template boundary_attribute_matches<BoundaryT>(mesh->GetBdrAttribute(boundaryElement))) {
continue;
}
finiteElementSpace.GetBdrElementVDofs(boundaryElement, boundaryElementVDofs);
for (const int encodedVDof : boundaryElementVDofs) {
const int vdof = mfem::FiniteElementSpace::DecodeDof(encodedVDof);
MFEM_VERIFY(
vdof >= 0 && vdof < finiteElementSpace.GetVSize(),
"MFEM returned an invalid scalar boundary vector DOF."
);
boundaryVDofMarker[vdof] = 1;
}
}
finiteElementSpace.Synchronize(boundaryVDofMarker);
mfem::Array<int> boundaryTrueDofMarker(finiteElementSpace.GetTrueVSize());
boundaryTrueDofMarker = 0;
for (int vdof = 0; vdof < boundaryVDofMarker.Size(); ++vdof) {
if (boundaryVDofMarker[vdof] == 0) {
continue;
}
const int trueDof = finiteElementSpace.GetLocalTDofNumber(vdof);
if (trueDof < 0) {
continue;
}
MFEM_VERIFY(trueDof < boundaryTrueDofMarker.Size(), "MFEM returned an invalid scalar boundary true DOF.");
boundaryTrueDofMarker[trueDof] = 1;
}
mfem::Array<int> boundaryTrueDofs;
mfem::FiniteElementSpace::MarkerToList(boundaryTrueDofMarker, boundaryTrueDofs);
const long long localSize = boundaryTrueDofs.Size();
long long globalSize = 0;
long long globalOffset = 0;
MPI_Allreduce(&localSize, &globalSize, 1, MPI_LONG_LONG, MPI_SUM, finiteElementSpace.GetComm());
MPI_Exscan(&localSize, &globalOffset, 1, MPI_LONG_LONG, MPI_SUM, finiteElementSpace.GetComm());
if (finiteElementSpace.GetMyRank() == 0) {
globalOffset = 0;
}
MFEM_VERIFY(globalSize > 0, "The requested semantic boundary has no scalar true DOFs.");
return ScalarBoundaryDofMap(finiteElementSpace.GetTrueVSize(), boundaryTrueDofs, globalOffset, globalSize);
}
template <utils::domain::IsSchema SchemaT = utils::domain::CoreEnvelopeVacuumDomainSchema>
requires(SchemaT::template contains_boundary<utils::domain::StellarSurface>())
[[nodiscard]] ScalarBoundaryDofMap
make_stellar_surface_scalar_dof_map(const mfem::ParFiniteElementSpace &finiteElementSpace) {
return make_scalar_boundary_dof_map<utils::domain::StellarSurface, SchemaT>(finiteElementSpace);
}
/*
* Boundary rows expressed in a field's reduced solver ordering.
*

View File

@@ -3,6 +3,10 @@ module;
#include <concepts>
#include <string_view>
#ifndef MEAN_FIELD_UNIFORM_POLYNOMIAL_ORDER_INCREMENT
#define MEAN_FIELD_UNIFORM_POLYNOMIAL_ORDER_INCREMENT 0
#endif
export module mean_field:field.registry;
export import :field.base;
@@ -10,13 +14,16 @@ export import :quadrature.policy;
export import :utils.domain;
export namespace mean_field::field {
inline constexpr int uniformPolynomialOrderIncrement = MEAN_FIELD_UNIFORM_POLYNOMIAL_ORDER_INCREMENT;
static_assert(uniformPolynomialOrderIncrement >= 0);
// =========================================================================
// Density
// =========================================================================
struct Density {
static constexpr std::string_view name = "density";
static constexpr int scalarOrder = 2;
static constexpr int scalarOrder = 2 + uniformPolynomialOrderIncrement;
using Support = DomainSupport<utils::domain::Stellar>;
@@ -79,8 +86,8 @@ export namespace mean_field::field {
struct Gravity {
static constexpr std::string_view name = "gravity";
static constexpr int potentialOrder = 2;
static constexpr int fluxOrder = 2;
static constexpr int potentialOrder = 2 + uniformPolynomialOrderIncrement;
static constexpr int fluxOrder = 2 + uniformPolynomialOrderIncrement;
using Support = DomainSupport<utils::domain::All>;
@@ -147,7 +154,7 @@ export namespace mean_field::field {
struct Displacement {
static constexpr std::string_view name = "displacement";
static constexpr int vectorOrder = 3;
static constexpr int vectorOrder = 3 + uniformPolynomialOrderIncrement;
using Support = DomainSupport<utils::domain::All>;
@@ -229,7 +236,7 @@ export namespace mean_field::field {
struct Enthalpy {
static constexpr std::string_view name = "specific_enthalpy";
static constexpr int scalarOrder = 3;
static constexpr int scalarOrder = 3 + uniformPolynomialOrderIncrement;
using Support = DomainSupport<utils::domain::Stellar>;

View File

@@ -65,6 +65,13 @@ export import :surface.constant;
export import :surface.dependencies;
export import :surface.compiled;
export import :surface.compiler;
export import :deformation.descriptors;
export import :deformation.surface_prescription;
export import :deformation.nodal_radial_surface;
export import :deformation.interior_extension;
export import :deformation.vacuum_extension;
export import :deformation.radial_extensions;
export import :deformation.domain_deformation;
export import :model.stellar;
export import :operators.prepared_mass_normalization;
export import :operators.prepared_centering_constraint;

View File

@@ -5,8 +5,11 @@ module;
#include <type_traits>
#include <utility>
#include <mfem.hpp>
export module mean_field:model.stellar;
export import :deformation.domain_deformation;
export import :eos.runtime;
export import :model.structure.base;
export import :surface.compiler;
@@ -35,16 +38,24 @@ export namespace mean_field::models {
std::remove_cvref_t<decltype(std::declval<const std::remove_cvref_t<Candidate> &>().equationOfState())>;
template <typename Candidate, typename EquationOfState>
concept SurfacePrescription =
concept SurfaceCondition =
surface::ConstantPressureSurfaceType<Candidate> &&
surface::PressureSurfaceCompilable<surface::BarotropicSurfaceFormulation, std::remove_cvref_t<EquationOfState>>;
template <StructurePrescription Structure>
requires SurfacePrescription<surface::ConstantPressureSurface, StructureEquationOfStateT<Structure>>
template <
StructurePrescription Structure,
deformation::SurfaceDeformationPrescription SurfaceDeformation = deformation::NodalRadialSurface,
deformation::InteriorDeformationExtension StellarInteriorDeformation =
deformation::PowerLawRadialInteriorExtension,
deformation::VacuumDeformationExtension VacuumDeformation = deformation::FixedInfinityRadialVacuumExtension>
requires SurfaceCondition<surface::ConstantPressureSurface, StructureEquationOfStateT<Structure>>
class StellarModel final {
public:
using StructurePrescriptionType = Structure;
using SurfacePrescriptionType = surface::ConstantPressureSurface;
using SurfaceConditionType = surface::ConstantPressureSurface;
using SurfaceDeformationPrescriptionType = SurfaceDeformation;
using StellarInteriorDeformationExtensionType = StellarInteriorDeformation;
using VacuumDeformationExtensionType = VacuumDeformation;
using EquationOfStateType = StructureEquationOfStateT<Structure>;
using SurfaceConstraintType =
surface::CompiledPressureSurfaceConstraintT<surface::BarotropicSurfaceFormulation, EquationOfStateType>;
@@ -55,18 +66,57 @@ export namespace mean_field::models {
Structure>
explicit StellarModel(
StructureArgument &&structurePrescription,
const surface::ConstantPressureSurface surfacePrescription
const surface::ConstantPressureSurface surfaceCondition
)
: StellarModel(
std::forward<StructureArgument>(structurePrescription),
surfaceCondition,
deformation::NodalRadialSurface{defaultReferenceCenter()},
deformation::PowerLawRadialInteriorExtension{},
deformation::FixedInfinityRadialVacuumExtension{}
) {
}
template <
typename StructureArgument,
typename SurfaceDeformationArgument,
typename StellarInteriorDeformationArgument,
typename VacuumDeformationArgument>
requires std::same_as<
std::remove_cvref_t<StructureArgument>,
Structure> &&
std::same_as<
std::remove_cvref_t<SurfaceDeformationArgument>,
SurfaceDeformation> &&
std::same_as<
std::remove_cvref_t<StellarInteriorDeformationArgument>,
StellarInteriorDeformation> &&
std::same_as<
std::remove_cvref_t<VacuumDeformationArgument>,
VacuumDeformation>
explicit StellarModel(
StructureArgument &&structurePrescription,
const surface::ConstantPressureSurface surfaceCondition,
SurfaceDeformationArgument &&surfaceDeformation,
StellarInteriorDeformationArgument &&stellarInteriorDeformation,
VacuumDeformationArgument &&vacuumDeformation
)
: m_structurePrescription(
std::make_unique<Structure>(std::forward<StructureArgument>(structurePrescription))
),
m_surfacePrescription(std::make_unique<surface::ConstantPressureSurface>(surfacePrescription)),
m_compiledSurfaceConstraint(
std::make_unique<SurfaceConstraintType>(validateAndCompileSurface(
*m_structurePrescription,
*m_surfacePrescription
))
) {
m_surfaceCondition(std::make_unique<surface::ConstantPressureSurface>(surfaceCondition)),
m_surfaceDeformation(
std::make_unique<SurfaceDeformation>(std::forward<SurfaceDeformationArgument>(surfaceDeformation))
),
m_stellarInteriorDeformation(
std::make_unique<StellarInteriorDeformation>(
std::forward<StellarInteriorDeformationArgument>(stellarInteriorDeformation)
)
),
m_vacuumDeformation(
std::make_unique<VacuumDeformation>(std::forward<VacuumDeformationArgument>(vacuumDeformation))
),
m_compiledSurfaceConstraint(std::make_unique<SurfaceConstraintType>(validateAndCompileConfiguration())) {
}
~StellarModel() = default;
@@ -80,8 +130,20 @@ export namespace mean_field::models {
return *m_structurePrescription;
}
[[nodiscard]] const surface::ConstantPressureSurface &surfacePrescription() const noexcept {
return *m_surfacePrescription;
[[nodiscard]] const surface::ConstantPressureSurface &surfaceCondition() const noexcept {
return *m_surfaceCondition;
}
[[nodiscard]] const SurfaceDeformation &surfaceDeformationPrescription() const noexcept {
return *m_surfaceDeformation;
}
[[nodiscard]] const StellarInteriorDeformation &stellarInteriorDeformationExtension() const noexcept {
return *m_stellarInteriorDeformation;
}
[[nodiscard]] const VacuumDeformation &vacuumDeformationExtension() const noexcept {
return *m_vacuumDeformation;
}
[[nodiscard]] const EquationOfStateType &equationOfState() const noexcept {
@@ -100,20 +162,36 @@ export namespace mean_field::models {
return *m_compiledSurfaceConstraint;
}
[[nodiscard]] auto compileDomainDeformation(fem::FEM &finiteElementModel) const {
return deformation::compileDomainDeformation(
surfaceDeformationPrescription(), stellarInteriorDeformationExtension(), vacuumDeformationExtension(),
finiteElementModel
);
}
private:
[[nodiscard]] static SurfaceConstraintType validateAndCompileSurface(
const Structure &structurePrescription,
const surface::ConstantPressureSurface &surfacePrescription
) {
structurePrescription.validate();
[[nodiscard]] static mfem::Vector defaultReferenceCenter() {
mfem::Vector center(3);
center = 0.0;
return center;
}
[[nodiscard]] SurfaceConstraintType validateAndCompileConfiguration() const {
m_structurePrescription->validate();
m_surfaceDeformation->validate();
m_stellarInteriorDeformation->validate();
m_vacuumDeformation->validate();
return surface::compilePressureSurfaceConstraint<surface::BarotropicSurfaceFormulation>(
surfacePrescription, structurePrescription.equationOfState()
*m_surfaceCondition, m_structurePrescription->equationOfState()
);
}
std::unique_ptr<Structure> m_structurePrescription;
std::unique_ptr<surface::ConstantPressureSurface> m_surfacePrescription;
std::unique_ptr<surface::ConstantPressureSurface> m_surfaceCondition;
std::unique_ptr<SurfaceDeformation> m_surfaceDeformation;
std::unique_ptr<StellarInteriorDeformation> m_stellarInteriorDeformation;
std::unique_ptr<VacuumDeformation> m_vacuumDeformation;
std::unique_ptr<SurfaceConstraintType> m_compiledSurfaceConstraint;
};
@@ -121,12 +199,42 @@ export namespace mean_field::models {
StellarModel(
Structure &&,
surface::ConstantPressureSurface
) -> StellarModel<std::remove_cvref_t<Structure>>;
)
-> StellarModel<
std::remove_cvref_t<Structure>,
deformation::NodalRadialSurface,
deformation::PowerLawRadialInteriorExtension,
deformation::FixedInfinityRadialVacuumExtension>;
template <
typename Structure,
typename SurfaceDeformation,
typename StellarInteriorDeformation,
typename VacuumDeformation>
StellarModel(
Structure &&,
surface::ConstantPressureSurface,
SurfaceDeformation &&,
StellarInteriorDeformation &&,
VacuumDeformation &&
)
-> StellarModel<
std::remove_cvref_t<Structure>,
std::remove_cvref_t<SurfaceDeformation>,
std::remove_cvref_t<StellarInteriorDeformation>,
std::remove_cvref_t<VacuumDeformation>>;
namespace detail {
template <typename Candidate> struct IsStellarModel : std::false_type { };
template <typename Structure> struct IsStellarModel<StellarModel<Structure>> : std::true_type { };
template <
typename Structure,
typename SurfaceDeformation,
typename StellarInteriorDeformation,
typename VacuumDeformation>
struct IsStellarModel<
StellarModel<Structure, SurfaceDeformation, StellarInteriorDeformation, VacuumDeformation>>
: std::true_type { };
} // namespace detail
template <typename Candidate>
@@ -143,7 +251,10 @@ export namespace mean_field::models {
m_makeInitialSeed(&makeInitialSeedFor<typename std::remove_cvref_t<Model>::StructurePrescriptionType>),
m_targetMass(model.targetMass()),
m_surfaceCondition(model.compiledSurfaceConstraint().descriptor()),
m_surfaceDependencies(model.compiledSurfaceConstraint().runtimeDependencies()) {
m_surfaceDependencies(model.compiledSurfaceConstraint().runtimeDependencies()),
m_surfaceDeformation(model.surfaceDeformationPrescription().descriptor()),
m_stellarInteriorDeformation(model.stellarInteriorDeformationExtension().descriptor()),
m_vacuumDeformation(model.vacuumDeformationExtension().descriptor()) {
}
[[nodiscard]] eos::EquationOfStateView equationOfState() const noexcept {
@@ -166,6 +277,18 @@ export namespace mean_field::models {
return m_surfaceDependencies;
}
[[nodiscard]] deformation::SurfaceDeformationDescriptor surfaceDeformation() const noexcept {
return m_surfaceDeformation;
}
[[nodiscard]] deformation::InteriorDeformationExtensionDescriptor stellarInteriorDeformation() const noexcept {
return m_stellarInteriorDeformation;
}
[[nodiscard]] deformation::VacuumDeformationExtensionDescriptor vacuumDeformation() const noexcept {
return m_vacuumDeformation;
}
private:
using MakeInitialSeedFunction = structure::StructureSeed (*)(
const void *,
@@ -186,5 +309,8 @@ export namespace mean_field::models {
double m_targetMass;
surface::PressureSurfaceDescriptor m_surfaceCondition;
surface::RuntimeSurfaceConstraintDependencies m_surfaceDependencies;
deformation::SurfaceDeformationDescriptor m_surfaceDeformation;
deformation::InteriorDeformationExtensionDescriptor m_stellarInteriorDeformation;
deformation::VacuumDeformationExtensionDescriptor m_vacuumDeformation;
};
} // namespace mean_field::models

View File

@@ -4,6 +4,7 @@ module;
#include <concepts>
#include <cstdint>
#include <type_traits>
#include <utility>
#include <mfem.hpp>
@@ -18,7 +19,6 @@ export import :operators.context.gravity_field;
export import :operators.gravity_field;
export import :operators.gravity_field_jacobian;
export import :operators.prepared_barotropic_closure;
export import :operators.prepared_centering_constraint;
export import :operators.prepared_displacement_residual;
export import :operators.prepared_hydrostatic_equilibrium;
export import :operators.prepared_mass_normalization;
@@ -37,7 +37,7 @@ export namespace mean_field::operators {
struct StellarEquilibriumDependencies final {
StellarEquilibriumDependencyStamp discretization;
StellarEquilibriumDependencyStamp density;
StellarEquilibriumDependencyStamp displacement;
StellarEquilibriumDependencyStamp surfaceDeformation;
StellarEquilibriumDependencyStamp gravityGradient;
StellarEquilibriumDependencyStamp gravityPotential;
StellarEquilibriumDependencyStamp enthalpy;
@@ -55,17 +55,18 @@ export namespace mean_field::operators {
PreparedDisplacementResidualReport displacement;
PreparedMassNormalizationReport massNormalization;
PreparedSurfaceConstraintReport surfaceConstraint;
PreparedCenteringConstraintReport centeringConstraint;
deformation::DomainDeformationGeometryReport generatedGeometry;
StellarEquilibriumDependencyStamp generatedDisplacement;
bool generatedVolumeDisplacement{false};
bool assembledResidual{false};
[[nodiscard]] bool DidAnyChildWork() const noexcept {
return gravity.DidAnyWork() || barotropicClosure.DidAnyWork() || hydrostatic.DidAnyWork() ||
displacement.DidAnyWork() || massNormalization.DidAnyWork() || surfaceConstraint.DidAnyWork() ||
centeringConstraint.DidAnyWork();
displacement.DidAnyWork() || massNormalization.DidAnyWork() || surfaceConstraint.DidAnyWork();
}
[[nodiscard]] bool DidAnyWork() const noexcept {
return DidAnyChildWork() || assembledResidual;
return DidAnyChildWork() || generatedVolumeDisplacement || assembledResidual;
}
};
@@ -73,11 +74,13 @@ export namespace mean_field::operators {
std::uint64_t residualAssemblies{0};
std::uint64_t residualApplications{0};
std::uint64_t jacobianApplications{0};
std::uint64_t generatedGeometryBuilds{0};
constexpr auto operator<=>(const PreparedStellarEquilibriumStatistics &) const = default;
};
using StellarEquilibriumLayout = utils::blocks::form_layout<utils::blocks::barotropic_equilibrium_form>;
using StellarEquilibriumLayout =
utils::blocks::form_layout<utils::blocks::surface_deformed_stellar_equilibrium_form>;
class PreparedStellarEquilibriumOperator final : public mfem::Operator {
public:
@@ -99,7 +102,8 @@ export namespace mean_field::operators {
domainMapper,
stellarModel.equationOfState(),
stellarModel.targetMass(),
PressureSurfaceConstraintView{stellarModel.compiledSurfaceConstraint()}
PressureSurfaceConstraintView{stellarModel.compiledSurfaceConstraint()},
deformation::PreparedDomainDeformationRuntime{stellarModel.compileDomainDeformation(f)}
) {
}
@@ -138,19 +142,27 @@ export namespace mean_field::operators {
[[nodiscard]] const PreparedDisplacementResidualOperator &GetDisplacementOperator() const noexcept;
[[nodiscard]] const PreparedMassNormalizationOperator &GetMassNormalizationOperator() const noexcept;
[[nodiscard]] const PreparedPressureSurfaceConstraint &GetSurfaceConstraintOperator() const noexcept;
[[nodiscard]] const PreparedCenteringConstraint &GetCenteringConstraintOperator() const noexcept;
[[nodiscard]] const deformation::PreparedDomainDeformationRuntime &GetDomainDeformation() const noexcept;
[[nodiscard]] const mfem::Vector &GetSurfaceDeformationParameters() const;
[[nodiscard]] const mfem::Vector &GetGeneratedVolumeDisplacement() const;
[[nodiscard]] const mfem::Vector &GetFullMechanicalResidual() const;
[[nodiscard]] const StellarEquilibriumDependencyStamp &GetGeneratedDisplacementDependency() const;
private:
struct ConstructionData;
static ConstructionData MakeConstructionData(fem::FEM &f);
static ConstructionData MakeConstructionData(
fem::FEM &f,
deformation::PreparedDomainDeformationRuntime domainDeformation
);
PreparedStellarEquilibriumOperator(
fem::FEM &f,
const mapping::DomainMapper &domainMapper,
const eos::Polytrope &equationOfState,
double targetMass,
PressureSurfaceConstraintView surfaceConstraint
PressureSurfaceConstraintView surfaceConstraint,
deformation::PreparedDomainDeformationRuntime domainDeformation
);
PreparedStellarEquilibriumOperator(
@@ -177,9 +189,14 @@ export namespace mean_field::operators {
PreparedDisplacementResidualOperator m_displacementOperator;
PreparedMassNormalizationOperator m_massNormalizationOperator;
PreparedPressureSurfaceConstraint m_surfaceConstraintOperator;
PreparedCenteringConstraint m_centeringConstraintOperator;
deformation::PreparedDomainDeformationRuntime m_domainDeformation;
StellarEquilibriumDependencies m_preparedDependencies;
StellarEquilibriumDependencyStamp m_generatedDisplacementDependency;
deformation::DomainDeformationGeometryReport m_generatedGeometryReport;
mfem::Vector m_surfaceDeformationParameters;
mfem::Vector m_generatedVolumeDisplacement;
mfem::Vector m_fullMechanicalResidual;
mfem::Vector m_cachedResidual;
double m_targetMass{0.0};
@@ -189,5 +206,9 @@ export namespace mean_field::operators {
mfem::Vector m_gravityState;
mutable mfem::Vector m_gravityDirection;
mutable mfem::Vector m_volumeDisplacementDirection;
mutable mfem::Vector m_fullMechanicalAction;
mutable mfem::Vector m_surfaceShapeAction;
mutable mfem::Vector m_pullbackDerivativeAction;
};
} // namespace mean_field::operators

View File

@@ -15,7 +15,7 @@ export namespace mean_field::surface {
};
/*
* The only physical surface prescription currently supported by
* The only physical surface condition currently supported by
* MeanField. It says nothing about which thermodynamic variable appears
* in a nonlinear state vector; resolving pressure into that representation
* is an EOS responsibility.

View File

@@ -59,6 +59,19 @@ export namespace mean_field::utils::blocks {
static inline constexpr geometry geometry_term{};
};
struct surface_deformation final : field {
struct parameters final : term {
struct value final : value_block_base { };
};
struct shape_equilibrium final : term {
struct residual final : residual_block_base { };
};
static inline constexpr parameters parameters_term{};
static inline constexpr shape_equilibrium shape_equilibrium_term{};
};
struct gravity final : field {
struct gradient final : term {
struct value final : value_block_base { };
@@ -99,6 +112,7 @@ export namespace mean_field::utils::blocks {
inline constexpr density density_field{};
inline constexpr displacement displacement_field{};
inline constexpr surface_deformation surface_deformation_field{};
inline constexpr gravity gravity_field{};
inline constexpr enthalpy enthalpy_field{};
inline constexpr barotropic_constant barotropic_constant_field{};
@@ -422,6 +436,59 @@ export namespace mean_field::utils::blocks {
density::mass::value,
displacement::geometry::value>>;
// Root stellar-equilibrium coordinates:
// [rho, q, g, Phi, h, C], where q parameterizes the stellar surface.
// Full-volume displacement remains an internal coordinate of the child
// operators above and is generated from q by the domain-deformation map.
using surface_deformed_stellar_equilibrium_form = block_form<
type_list<
density::mass::value,
surface_deformation::parameters::value,
gravity::gradient::value,
gravity::poisson::value,
enthalpy::specific::value,
barotropic_constant::mass_normalization::value>,
type_list<
gravity::gradient::residual,
gravity::poisson::residual,
density::mass::residual,
surface_deformation::shape_equilibrium::residual,
enthalpy::specific::residual,
barotropic_constant::mass_normalization::residual>>;
using surface_deformed_stellar_equilibrium_jacobian_form = type_list<
block_row<
gravity::gradient::residual,
gravity::gradient::value,
gravity::poisson::value,
surface_deformation::parameters::value>,
block_row<
gravity::poisson::residual,
gravity::gradient::value,
density::mass::value,
surface_deformation::parameters::value>,
block_row<
density::mass::residual,
density::mass::value,
enthalpy::specific::value,
surface_deformation::parameters::value>,
block_row<
surface_deformation::shape_equilibrium::residual,
density::mass::value,
surface_deformation::parameters::value,
gravity::gradient::value,
enthalpy::specific::value>,
block_row<
enthalpy::specific::residual,
enthalpy::specific::value,
gravity::poisson::value,
surface_deformation::parameters::value,
barotropic_constant::mass_normalization::value>,
block_row<
barotropic_constant::mass_normalization::residual,
density::mass::value,
surface_deformation::parameters::value>>;
// Columns: [d, h]
// Rows: [R_d]
using pressure_force_form = block_form<
@@ -438,4 +505,8 @@ export namespace mean_field::utils::blocks {
static_assert(valid_jacobian_form<
barotropic_equilibrium_form,
barotropic_equilibrium_jacobian_form>);
static_assert(valid_jacobian_form<
surface_deformed_stellar_equilibrium_form,
surface_deformed_stellar_equilibrium_jacobian_form>);
} // namespace mean_field::utils::blocks

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

@@ -390,7 +390,7 @@ namespace stellar_equilibrium_test_utils {
return {
.discretization = {.identity = 1009, .revision = 3},
.density = {.identity = 1013, .revision = 5},
.displacement = {.identity = 1019, .revision = 7},
.surfaceDeformation = {.identity = 1019, .revision = 7},
.gravityGradient = {.identity = 1021, .revision = 11},
.gravityPotential = {.identity = 1031, .revision = 13},
.enthalpy = {.identity = 1033, .revision = 17},
@@ -402,7 +402,7 @@ namespace stellar_equilibrium_test_utils {
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;
@@ -552,7 +561,11 @@ 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 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);
@@ -848,74 +882,24 @@ TEST_CASE(
);
const auto &layout = stellarOperator.GetLayout();
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 mfem::Vector state = stellar_equilibrium_test_utils::make_state(f, layout);
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

@@ -319,6 +319,52 @@ export namespace tags {
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");
@@ -414,8 +460,11 @@ export namespace tags {
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_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