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

@@ -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;
@@ -23,7 +22,7 @@ import experiment;
using namespace experiment;
static std::string escape_csv(const std::string& value) {
static std::string escape_csv(const std::string &value) {
if (value.find_first_of(",\"\n") == std::string::npos) {
return value;
}
@@ -51,11 +50,11 @@ static void write_experiment_csv() {
std::set<std::string> parameter_names;
std::set<std::string> metric_names;
for (const ExperimentResult& result : results) {
for (const auto& [name, value] : result.parameters) {
for (const ExperimentResult &result : results) {
for (const auto &[name, value] : result.parameters) {
parameter_names.insert(name);
}
for (const auto& [name, value] : result.metrics) {
for (const auto &[name, value] : result.metrics) {
metric_names.insert(name);
}
}
@@ -68,22 +67,22 @@ static void write_experiment_csv() {
}
output << "experiment,case";
for (const std::string& name : parameter_names) {
for (const std::string &name : parameter_names) {
output << ',' << escape_csv(name);
}
for (const std::string& name : metric_names) {
for (const std::string &name : metric_names) {
output << ',' << escape_csv(name);
}
output << '\n';
output << std::setprecision(17);
for (const ExperimentResult& result : results) {
for (const ExperimentResult &result : results) {
output << escape_csv(result.experiment_name) << ',' << escape_csv(result.case_name);
for (const std::string& name : parameter_names) {
for (const std::string &name : parameter_names) {
const auto iterator = result.parameters.find(name);
output << ',' << (iterator == result.parameters.end() ? "" : escape_csv(iterator->second));
}
for (const std::string& name : metric_names) {
for (const std::string &name : metric_names) {
const auto iterator = result.metrics.find(name);
output << ',';
if (iterator != result.metrics.end()) {
@@ -104,24 +103,28 @@ public:
return "Compact console reporter that writes structured experiment measurements to CSV.";
}
void testCaseEnded(const Catch::TestCaseStats& statistics) override {
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 {
void testRunEnded(const Catch::TestRunStats &statistics) override {
StreamingReporterBase::testRunEnded(statistics);
write_experiment_csv();
}
};
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"};
@@ -149,39 +152,39 @@ int main(int argc, char* argv[]) {
}
}
std::vector<const char*> configuration_argv;
std::vector<const char *> configuration_argv;
configuration_argv.reserve(configuration_arguments.size());
for (const std::string& argument : configuration_arguments) {
for (const std::string &argument : configuration_arguments) {
configuration_argv.push_back(argument.c_str());
}
try {
app.parse(static_cast<int>(configuration_argv.size()), configuration_argv.data());
} catch (const CLI::ParseError& error) {
} catch (const CLI::ParseError &error) {
return app.exit(error);
}
std::vector<std::string> catch_arguments{argv[0]};
for (const std::string& argument : app.remaining()) {
for (const std::string &argument : app.remaining()) {
catch_arguments.push_back(argument);
}
for (const std::string& argument : catch_arguments_from_command_line) {
for (const std::string &argument : catch_arguments_from_command_line) {
catch_arguments.push_back(argument);
}
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=");
for (const std::string &argument : catch_arguments) {
has_reporter = has_reporter || argument == "-r" || argument == "--reporter" || argument.starts_with("-r=") ||
argument.starts_with("--reporter=");
}
if (!has_reporter) {
catch_arguments.emplace_back("--reporter");
catch_arguments.emplace_back("experiment");
}
std::vector<const char*> catch_argv;
std::vector<const char *> catch_argv;
catch_argv.reserve(catch_arguments.size());
for (const std::string& argument : catch_arguments) {
for (const std::string &argument : catch_arguments) {
catch_argv.push_back(argument.c_str());
}

View File

@@ -18,7 +18,7 @@ export namespace experiment {
class ExperimentRegistry {
public:
static ExperimentRegistry& instance() {
static ExperimentRegistry &instance() {
static ExperimentRegistry registry;
return registry;
}
@@ -50,16 +50,20 @@ 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
const std::string &experiment_name,
const std::string &case_name,
std::map<
std::string,
std::string> parameters,
std::map<
std::string,
double> metrics
) {
ExperimentRegistry::instance().add_result({
.experiment_name = experiment_name,
.case_name = case_name,
.parameters = std::move(parameters),
.metrics = std::move(metrics)
});
ExperimentRegistry::instance().add_result(
{.experiment_name = experiment_name,
.case_name = case_name,
.parameters = std::move(parameters),
.metrics = std::move(metrics)}
);
}
}
} // namespace experiment

File diff suppressed because it is too large Load Diff

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);
const mfem::Vector action = fixture.jacobian_action(mode.direction);
const mfem::Vector liftedDirection = fixture.lifted_surface_direction(mode.direction);
mfem::Vector centeringContribution(constrainedAction);
centeringContribution -= unpinnedAction;
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 inputNorm = experiment::null_space::global_norm(mode.direction, 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,27 +122,27 @@ 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;
}
}
[[nodiscard]] inline mean_field::operators::StellarEquilibriumDependencies make_dependencies() {
return {
.discretization = {.identity = 2003, .revision = 1},
.density = {.identity = 2011, .revision = 1},
.displacement = {.identity = 2017, .revision = 1},
.gravityGradient = {.identity = 2027, .revision = 1},
.gravityPotential = {.identity = 2029, .revision = 1},
.enthalpy = {.identity = 2039, .revision = 1},
.bernoulliConstant = {.identity = 2053, .revision = 1},
.rotation = {.identity = 2063, .revision = 1},
.targetMass = {.identity = 2069, .revision = 1}
.discretization = {.identity = 2003, .revision = 1},
.density = {.identity = 2011, .revision = 1},
.surfaceDeformation = {.identity = 2017, .revision = 1},
.gravityGradient = {.identity = 2027, .revision = 1},
.gravityPotential = {.identity = 2029, .revision = 1},
.enthalpy = {.identity = 2039, .revision = 1},
.bernoulliConstant = {.identity = 2053, .revision = 1},
.rotation = {.identity = 2063, .revision = 1},
.targetMass = {.identity = 2069, .revision = 1}
};
}
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 {
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);
[[nodiscard]] mfem::Vector jacobian_action(const mfem::Vector &direction) const {
mfem::Vector result;
m_operator.Mult(direction, result);
return result;
}
const mfem::Vector gravityDirection = pack_gravity_state(
reducedDensityDirection, displacementDirection, gravityGradientDirection, gravityPotentialDirection
[[nodiscard]] mfem::Vector lifted_surface_direction(const mfem::Vector &rootDirection) const {
const auto &layout = m_operator.GetLayout();
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();
const auto &layout = fixture.stellar_operator().GetLayout();
std::array<RigidMode, 6> modes;
[[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";
}
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);
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)
};
[[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;
}
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);
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();
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, rotationTrue);
modes[3 + axis] = RigidMode{
.name = std::string("rotation_") + static_cast<char>('x' + axis),
.kind = RigidModeKind::rotation,
.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)}
);
}
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)
};