Files
MeanField/tests/operators/prepared_stellar_equilibrium.cpp
Emily Boudreaux 36adfa1174 feat(FieldDofMap): Completed FieldDofMap migration
also removed legacy BarotropicPolytrope implementation
2026-08-29 08:56:36 -04:00

2440 lines
93 KiB
C++

#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <limits>
#include <type_traits>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
#include <mpi.h>
import mean_field;
import test_helpers;
namespace stellar_equilibrium_test_utils {
using Form = mean_field::utils::blocks::barotropic_equilibrium_form;
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
namespace field = mean_field::field;
struct FieldMaps final {
field::FieldDofMap density;
field::FieldDofMap displacement;
field::FieldDofMap gravityFlux;
field::FieldDofMap gravityPotential;
field::FieldDofMap enthalpy;
explicit FieldMaps(const mean_field::fem::FEM &f)
: density(field::make_field_dof_map<field::Density, DomainSchema>(
*f.densityFes)),
displacement(
field::make_field_dof_map<field::Displacement, DomainSchema>(
*f.displacementFes)),
gravityFlux(field::make_field_dof_map<field::Gravity, DomainSchema>(
*f.gravityFluxFes)),
gravityPotential(
field::make_field_dof_map<field::Gravity, DomainSchema>(
*f.gravityPotentialFes)),
enthalpy(field::make_field_dof_map<field::Enthalpy, DomainSchema>(
*f.enthalpyFes)) {}
};
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 gravityGradientValue =
mean_field::utils::blocks::get_value_block<Form>(
mean_field::utils::blocks::gravity_field.gradient_term);
constexpr auto gravityPotentialValue =
mean_field::utils::blocks::get_value_block<Form>(
mean_field::utils::blocks::gravity_field.poisson_term);
constexpr auto enthalpyValue = mean_field::utils::blocks::get_value_block<Form>(
mean_field::utils::blocks::enthalpy_field.specific_term);
constexpr auto bernoulliValue =
mean_field::utils::blocks::get_value_block<Form>(
mean_field::utils::blocks::barotropic_constant_field
.mass_normalization_term);
constexpr auto gravityGradientResidual =
mean_field::utils::blocks::get_residual_block<Form>(
mean_field::utils::blocks::gravity_field.gradient_term);
constexpr auto gravityPotentialResidual =
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 enthalpyResidual =
mean_field::utils::blocks::get_residual_block<Form>(
mean_field::utils::blocks::enthalpy_field.specific_term);
constexpr auto massResidual =
mean_field::utils::blocks::get_residual_block<Form>(
mean_field::utils::blocks::barotropic_constant_field
.mass_normalization_term);
template <int index>
[[nodiscard]] mfem::Vector
value_view(mfem::Vector &vector,
const mean_field::operators::StellarEquilibriumLayout &layout,
const mean_field::utils::blocks::value_block<index> block) {
return mfem::Vector(vector.GetData() + layout.offset(block),
layout.size(block));
}
template <int index>
void assign_value_block(
mfem::Vector &vector,
const mean_field::operators::StellarEquilibriumLayout &layout,
const mean_field::utils::blocks::value_block<index> block,
const mfem::Vector &source) {
MFEM_VERIFY(source.Size() == layout.size(block),
"Source vector has the wrong size for the coupled value block.");
const int offset = layout.offset(block);
for (int dof = 0; dof < source.Size(); ++dof) {
vector(offset + dof) = source(dof);
}
}
template <int index>
[[nodiscard]] mfem::Vector
const_value_view(const mfem::Vector &vector,
const mean_field::operators::StellarEquilibriumLayout &layout,
const mean_field::utils::blocks::value_block<index> block) {
return mfem::Vector(const_cast<mfem::real_t *>(vector.GetData()) +
layout.offset(block),
layout.size(block));
}
template <int index>
[[nodiscard]] mfem::Vector
residual_view(mfem::Vector &vector,
const mean_field::operators::StellarEquilibriumLayout &layout,
const mean_field::utils::blocks::residual_block<index> block) {
return mfem::Vector(vector.GetData() + layout.offset(block),
layout.size(block));
}
template <int index>
[[nodiscard]] mfem::Vector const_residual_view(
const mfem::Vector &vector,
const mean_field::operators::StellarEquilibriumLayout &layout,
const mean_field::utils::blocks::residual_block<index> block) {
return mfem::Vector(const_cast<mfem::real_t *>(vector.GetData()) +
layout.offset(block),
layout.size(block));
}
[[nodiscard]] mfem::Vector reduce_density(const mean_field::fem::FEM &f,
const mfem::Vector &fullDensity) {
const field::FieldDofMap map =
field::make_field_dof_map<field::Density, DomainSchema>(*f.densityFes);
return map.gather(fullDensity);
}
[[nodiscard]] mfem::Vector reduce_enthalpy(const mean_field::fem::FEM &f,
const mfem::Vector &fullEnthalpy) {
const field::FieldDofMap map =
field::make_field_dof_map<field::Enthalpy, DomainSchema>(*f.enthalpyFes);
return map.gather(fullEnthalpy);
}
[[nodiscard]] mfem::Vector pack_gravity_state(
const mfem::Vector &density, const mfem::Vector &displacement,
const mfem::Vector &gravityGradient, const mfem::Vector &gravityPotential) {
const std::array<int, 4> blockSizes{density.Size(), displacement.Size(),
gravityGradient.Size(),
gravityPotential.Size()};
const std::array<int, 5> offsets{
0, blockSizes[0], blockSizes[0] + blockSizes[1],
blockSizes[0] + blockSizes[1] + blockSizes[2],
blockSizes[0] + blockSizes[1] + blockSizes[2] + blockSizes[3]};
mfem::Vector packed(offsets[4]);
const std::array<const mfem::Vector *, 4> blocks{
&density, &displacement, &gravityGradient, &gravityPotential};
for (int block = 0; block < 4; ++block) {
mfem::Vector destination(packed.GetData() + offsets[block],
blockSizes[block]);
destination = *blocks[block];
}
return packed;
}
[[nodiscard]] mfem::Vector project_density(const mean_field::fem::FEM &f,
const double phase) {
mfem::ParGridFunction field(f.densityFes.get());
mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) {
return 0.88 + 0.07 * std::sin(0.73 * position(0) + phase) +
0.05 * std::cos(0.59 * position(1) - phase) +
0.025 * position(2) * position(2);
});
field.ProjectCoefficient(coefficient);
mfem::Vector result;
field.GetTrueDofs(result);
return result;
}
[[nodiscard]] mfem::Vector
project_density_direction(const mean_field::fem::FEM &f, const double phase) {
mfem::ParGridFunction field(f.densityFes.get());
mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) {
return 0.16 * std::sin(0.91 * position(0) + phase) -
0.12 * std::cos(0.77 * position(1) - phase) + 0.06 * position(2);
});
field.ProjectCoefficient(coefficient);
mfem::Vector result;
field.GetTrueDofs(result);
return result;
}
[[nodiscard]] mfem::Vector
project_constant_density(const mean_field::fem::FEM &f, const double value) {
mfem::ParGridFunction field(f.densityFes.get());
mfem::ConstantCoefficient coefficient(value);
field.ProjectCoefficient(coefficient);
mfem::Vector result;
field.GetTrueDofs(result);
return result;
}
[[nodiscard]] mfem::Vector project_displacement(const mean_field::fem::FEM &f,
const double scale) {
return gravity_prepared_test_utils::make_displacement(f, scale);
}
[[nodiscard]] mfem::Vector
project_displacement_direction(const mean_field::fem::FEM &f,
const double scale) {
mfem::ParGridFunction field(f.displacementFes.get());
mfem::VectorFunctionCoefficient coefficient(
f.mesh->Dimension(),
[scale](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(3);
value(0) =
scale * (0.06 * position(0) + 0.014 * position(1) * position(2));
value(1) =
scale * (-0.045 * position(1) + 0.011 * position(0) * position(2));
value(2) =
scale * (0.035 * position(2) - 0.009 * position(0) * position(1));
});
field.ProjectCoefficient(coefficient);
mfem::Vector result;
field.GetTrueDofs(result);
return result;
}
[[nodiscard]] mfem::Vector
project_gravity_gradient(const mean_field::fem::FEM &f, const double phase) {
mfem::ParGridFunction field(f.gravityFluxFes.get());
mfem::VectorFunctionCoefficient coefficient(
f.mesh->Dimension(),
[phase](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(3);
value(0) = 0.27 + 0.07 * position(0) + 0.025 * phase * position(1);
value(1) = -0.19 + 0.055 * position(1) - 0.018 * phase * position(2);
value(2) = 0.21 - 0.045 * position(2) + 0.021 * phase * position(0);
});
field.ProjectCoefficient(coefficient);
mfem::Vector result;
field.GetTrueDofs(result);
return result;
}
[[nodiscard]] mfem::Vector
project_gravity_direction(const mean_field::fem::FEM &f, const double phase) {
mfem::ParGridFunction field(f.gravityFluxFes.get());
mfem::VectorFunctionCoefficient coefficient(
f.mesh->Dimension(),
[phase](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(3);
value(0) = 0.13 * std::sin(position(0) + phase) + 0.025 * position(1);
value(1) = -0.10 * std::cos(position(1) - phase) + 0.035 * position(2);
value(2) =
0.08 * std::sin(position(2) + 0.5 * phase) - 0.018 * position(0);
});
field.ProjectCoefficient(coefficient);
mfem::Vector result;
field.GetTrueDofs(result);
return result;
}
[[nodiscard]] mfem::Vector
project_gravity_potential(const mean_field::fem::FEM &f, const double phase) {
mfem::ParGridFunction field(f.gravityPotentialFes.get());
mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) {
return 0.24 + 0.09 * std::sin(0.67 * position(0) + phase) -
0.06 * std::cos(0.53 * position(1) - phase) + 0.035 * position(2);
});
field.ProjectCoefficient(coefficient);
mfem::Vector result;
field.GetTrueDofs(result);
return result;
}
[[nodiscard]] mfem::Vector
project_potential_direction(const mean_field::fem::FEM &f, const double phase) {
mfem::ParGridFunction field(f.gravityPotentialFes.get());
mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) {
return 0.17 * std::sin(0.81 * position(0) + phase) +
0.11 * std::cos(0.69 * position(1) - phase) - 0.07 * position(2);
});
field.ProjectCoefficient(coefficient);
mfem::Vector result;
field.GetTrueDofs(result);
return result;
}
[[nodiscard]] mfem::Vector project_enthalpy(const mean_field::fem::FEM &f,
const double phase) {
mfem::ParGridFunction field(f.enthalpyFes.get());
mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) {
return 0.82 + 0.08 * std::sin(0.62 * position(0) + phase) +
0.045 * std::cos(0.57 * position(1) - phase) +
0.02 * position(2) * position(2);
});
field.ProjectCoefficient(coefficient);
mfem::Vector result;
field.GetTrueDofs(result);
return result;
}
[[nodiscard]] mfem::Vector
project_enthalpy_direction(const mean_field::fem::FEM &f, const double phase) {
mfem::ParGridFunction field(f.enthalpyFes.get());
mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) {
return 0.21 * std::sin(0.74 * position(0) + phase) -
0.14 * std::cos(0.64 * position(1) - phase) + 0.075 * position(2);
});
field.ProjectCoefficient(coefficient);
mfem::Vector result;
field.GetTrueDofs(result);
return result;
}
[[nodiscard]] mfem::Vector
project_constant_scalar(mfem::ParFiniteElementSpace &finiteElementSpace,
const double value) {
mfem::ParGridFunction field(&finiteElementSpace);
mfem::ConstantCoefficient coefficient(value);
field.ProjectCoefficient(coefficient);
mfem::Vector result;
field.GetTrueDofs(result);
return result;
}
[[nodiscard]] mean_field::physics::RigidRotation
make_rotation(const double scale) {
mfem::Vector angularVelocity(3);
angularVelocity(0) = scale * 0.16;
angularVelocity(1) = scale * -0.08;
angularVelocity(2) = scale * 0.58;
mfem::Vector center(3);
center(0) = 0.03;
center(1) = -0.025;
center(2) = 0.015;
return mean_field::physics::RigidRotation(angularVelocity, center);
}
[[nodiscard]] mean_field::physics::RigidRotation make_zero_rotation() {
return make_rotation(0.0);
}
[[nodiscard]] mean_field::operators::StellarEquilibriumDependencies
make_dependencies() {
return {.discretization = {.identity = 1009, .revision = 3},
.density = {.identity = 1013, .revision = 5},
.displacement = {.identity = 1019, .revision = 7},
.gravityGradient = {.identity = 1021, .revision = 11},
.gravityPotential = {.identity = 1031, .revision = 13},
.enthalpy = {.identity = 1033, .revision = 17},
.bernoulliConstant = {.identity = 1039, .revision = 19},
.rotation = {.identity = 1049, .revision = 23},
.targetMass = {.identity = 1051, .revision = 29}};
}
void increment_all_state_revisions(
mean_field::operators::StellarEquilibriumDependencies &dependencies) {
++dependencies.density.revision;
++dependencies.displacement.revision;
++dependencies.gravityGradient.revision;
++dependencies.gravityPotential.revision;
++dependencies.enthalpy.revision;
++dependencies.bernoulliConstant.revision;
}
[[nodiscard]] mfem::Vector
make_state(const mean_field::fem::FEM &f,
const mean_field::operators::StellarEquilibriumLayout &layout) {
const FieldMaps maps(f);
mfem::Vector state(layout.value_offsets().Last());
state = 0.0;
{
const mfem::Vector fullDensity = project_density(f, 0.31);
const mfem::Vector reducedDensity = maps.density.gather(fullDensity);
assign_value_block(state, layout, densityValue, reducedDensity);
}
assign_value_block(state, layout, displacementValue,
project_displacement(f, 0.63));
assign_value_block(state, layout, gravityGradientValue,
project_gravity_gradient(f, 0.43));
assign_value_block(state, layout, gravityPotentialValue,
project_gravity_potential(f, 0.47));
{
const mfem::Vector fullEnthalpy = project_enthalpy(f, 0.53);
const mfem::Vector reducedEnthalpy = maps.enthalpy.gather(fullEnthalpy);
assign_value_block(state, layout, enthalpyValue, reducedEnthalpy);
}
value_view(state, layout, bernoulliValue)(0) = 1.07;
return state;
}
[[nodiscard]] mfem::Vector
make_direction(const mean_field::fem::FEM &f,
const mean_field::operators::StellarEquilibriumLayout &layout) {
const FieldMaps maps(f);
mfem::Vector direction(layout.value_offsets().Last());
direction = 0.0;
{
const mfem::Vector fullDensityDirection =
project_density_direction(f, 0.61);
const mfem::Vector reducedDensityDirection =
maps.density.gather(fullDensityDirection);
assign_value_block(direction, layout, densityValue,
reducedDensityDirection);
}
assign_value_block(direction, layout, displacementValue,
project_displacement_direction(f, 0.79));
assign_value_block(direction, layout, gravityGradientValue,
project_gravity_direction(f, 0.83));
assign_value_block(direction, layout, gravityPotentialValue,
project_potential_direction(f, 0.89));
{
const mfem::Vector fullEnthalpyDirection =
project_enthalpy_direction(f, 0.97);
const mfem::Vector reducedEnthalpyDirection =
maps.enthalpy.gather(fullEnthalpyDirection);
assign_value_block(direction, layout, enthalpyValue,
reducedEnthalpyDirection);
}
value_view(direction, layout, bernoulliValue)(0) = -0.37;
return direction;
}
[[nodiscard]] double global_norm(const mfem::Vector &vector,
const MPI_Comm communicator) {
return gravity_prepared_test_utils::global_norm(vector, communicator);
}
[[nodiscard]] double relative_difference(const mfem::Vector &left,
const mfem::Vector &right,
const MPI_Comm communicator) {
mfem::Vector difference(left);
difference -= right;
const double scale = std::max(
{global_norm(left, communicator), global_norm(right, communicator),
100.0 * std::numeric_limits<double>::epsilon()});
return global_norm(difference, communicator) / scale;
}
[[nodiscard]] mfem::Vector explicit_residual(
const mean_field::operators::PreparedStellarEquilibriumOperator
&stellarOperator,
const mean_field::fem::FEM &f, const mfem::Vector &state) {
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 gravityGradient =
const_value_view(state, layout, gravityGradientValue);
const mfem::Vector gravityPotential =
const_value_view(state, layout, gravityPotentialValue);
const mfem::Vector gravityState = pack_gravity_state(
reducedDensity, displacement, gravityGradient, gravityPotential);
mfem::Vector gravity;
mfem::Vector closure;
mfem::Vector displacementResidualValue;
mfem::Vector hydrostatic;
mfem::Vector mass;
stellarOperator.GetGravityOperator().Mult(gravityState, gravity);
stellarOperator.GetBarotropicClosureOperator().BuildResidual(closure);
stellarOperator.GetDisplacementOperator().BuildResidual(
displacementResidualValue);
stellarOperator.GetHydrostaticOperator().BuildResidual(hydrostatic);
stellarOperator.GetMassNormalizationOperator().BuildResidual(mass);
mfem::Vector result(layout.residual_offsets().Last());
result = 0.0;
MFEM_VERIFY(gravity.Size() == layout.size(gravityGradientResidual) +
layout.size(gravityPotentialResidual),
"Explicit gravity residual has the wrong size.");
const mfem::Vector gravityGradientResidualValue(
gravity.GetData(), layout.size(gravityGradientResidual));
const mfem::Vector gravityPotentialResidualValue(
gravity.GetData() + layout.size(gravityGradientResidual),
layout.size(gravityPotentialResidual));
residual_view(result, layout, gravityGradientResidual) =
gravityGradientResidualValue;
residual_view(result, layout, gravityPotentialResidual) =
gravityPotentialResidualValue;
residual_view(result, layout, densityResidual) = closure;
residual_view(result, layout, displacementResidual) =
displacementResidualValue;
residual_view(result, layout, enthalpyResidual) = hydrostatic;
residual_view(result, layout, massResidual) = mass;
return result;
}
[[nodiscard]] mfem::Vector explicit_jacobian_action(
const mean_field::operators::PreparedStellarEquilibriumOperator
&stellarOperator,
const mfem::Vector &direction) {
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 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);
mfem::Vector gravityAction;
mfem::Vector closureAction;
mfem::Vector displacementAction;
mfem::Vector hydrostaticAction;
mfem::Vector massAction;
stellarOperator.GetGravityJacobianOperator().Mult(gravityDirection,
gravityAction);
stellarOperator.GetBarotropicClosureOperator().Mult(
reducedDensityDirection, reducedEnthalpyDirection, displacementDirection,
closureAction);
stellarOperator.GetDisplacementOperator().ApplyCompleteJacobianAction(
reducedDensityDirection, displacementDirection, gravityGradientDirection,
reducedEnthalpyDirection, displacementAction);
stellarOperator.GetHydrostaticOperator().ApplyCompleteJacobianAction(
reducedEnthalpyDirection, gravityPotentialDirection,
bernoulliDirection(0), displacementDirection, hydrostaticAction);
stellarOperator.GetMassNormalizationOperator().ApplyCompleteJacobianAction(
reducedDensityDirection, displacementDirection, massAction);
mfem::Vector result(layout.residual_offsets().Last());
result = 0.0;
MFEM_VERIFY(gravityAction.Size() == layout.size(gravityGradientResidual) +
layout.size(gravityPotentialResidual),
"Explicit gravity Jacobian action has the wrong size.");
const mfem::Vector gravityGradientAction(
gravityAction.GetData(), layout.size(gravityGradientResidual));
const mfem::Vector gravityPotentialAction(
gravityAction.GetData() + layout.size(gravityGradientResidual),
layout.size(gravityPotentialResidual));
residual_view(result, layout, gravityGradientResidual) =
gravityGradientAction;
residual_view(result, layout, gravityPotentialResidual) =
gravityPotentialAction;
residual_view(result, layout, densityResidual) = closureAction;
residual_view(result, layout, displacementResidual) = displacementAction;
residual_view(result, layout, enthalpyResidual) = hydrostaticAction;
residual_view(result, layout, massResidual) = massAction;
return result;
}
[[nodiscard]] long long global_sum(const int localValue,
const MPI_Comm communicator) {
const long long local = static_cast<long long>(localValue);
long long global = 0;
MPI_Allreduce(&local, &global, 1, MPI_LONG_LONG, MPI_SUM, communicator);
return global;
}
template <int index>
[[nodiscard]] double block_relative_difference(
const mfem::Vector &left, const mfem::Vector &right,
const mean_field::operators::StellarEquilibriumLayout &layout,
const mean_field::utils::blocks::residual_block<index> block,
const MPI_Comm communicator) {
return relative_difference(const_residual_view(left, layout, block),
const_residual_view(right, layout, block),
communicator);
}
} // 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) {
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);
mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator(
f, *f.domainMapperStateless, barotrope, 1.0);
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::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());
CHECK(layout.size(stellar_equilibrium_test_utils::bernoulliValue) == 1);
CHECK(layout.size(stellar_equilibrium_test_utils::gravityGradientResidual) ==
maps.gravityFlux.reduced_size());
CHECK(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::enthalpyResidual) ==
maps.enthalpy.reduced_size());
CHECK(layout.size(stellar_equilibrium_test_utils::massResidual) == 1);
CHECK(maps.displacement.is_identity());
CHECK(maps.gravityFlux.is_identity());
CHECK(maps.gravityPotential.is_identity());
CHECK(stellarOperator.Width() == layout.value_offsets().Last());
CHECK(stellarOperator.Height() == layout.residual_offsets().Last());
const MPI_Comm communicator = f.mesh->GetComm();
const long long globalDensityFull =
stellar_equilibrium_test_utils::global_sum(maps.density.full_size(),
communicator);
const long long globalDensityReduced =
stellar_equilibrium_test_utils::global_sum(maps.density.reduced_size(),
communicator);
const long long globalEnthalpyFull =
stellar_equilibrium_test_utils::global_sum(maps.enthalpy.full_size(),
communicator);
const long long globalEnthalpyReduced =
stellar_equilibrium_test_utils::global_sum(maps.enthalpy.reduced_size(),
communicator);
INFO("Global density full true DOFs = " << globalDensityFull);
INFO("Global density solver DOFs = " << globalDensityReduced);
INFO("Global enthalpy full true DOFs = " << globalEnthalpyFull);
INFO("Global enthalpy solver DOFs = " << globalEnthalpyReduced);
REQUIRE(globalDensityFull > 0);
REQUIRE(globalEnthalpyFull > 0);
CHECK(globalDensityReduced > 0);
CHECK(globalDensityReduced < globalDensityFull);
CHECK(globalEnthalpyReduced > 0);
CHECK(globalEnthalpyReduced < globalEnthalpyFull);
}
TEST_CASE("Prepared Stellar Equilibrium Jacobian Is The Exact Restricted Full "
"Child Jacobian",
tags::barotrope_prepared_jacobian_accuracy &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);
REQUIRE(f.okay());
const mean_field::eos::Polytrope barotrope(3.0, 0.25);
mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator(
f, *f.domainMapperStateless, barotrope, 1.19);
const auto &layout = stellarOperator.GetLayout();
const mfem::Vector state =
stellar_equilibrium_test_utils::make_state(f, layout);
const mfem::Vector direction =
stellar_equilibrium_test_utils::make_direction(f, layout);
const auto dependencies = stellar_equilibrium_test_utils::make_dependencies();
const auto rotation = stellar_equilibrium_test_utils::make_rotation(0.82);
stellarOperator.Prepare(state, dependencies, rotation);
mfem::Vector rootAction;
stellarOperator.Mult(direction, rootAction);
const mfem::Vector explicitAction =
stellar_equilibrium_test_utils::explicit_jacobian_action(stellarOperator,
direction);
const double difference = stellar_equilibrium_test_utils::relative_difference(
rootAction, explicitAction, f.mesh->GetComm());
INFO("Reduced root versus explicit R J P relative difference = "
<< difference);
CHECK(difference < 2.0e-15);
}
TEST_CASE(
"Prepared Stellar Equilibrium Owns And Composes Every Fixed Residual Row",
tags::barotrope &tags::prepared &tags::integration &tags::residuals) {
using Operator = mean_field::operators::PreparedStellarEquilibriumOperator;
STATIC_REQUIRE_FALSE(std::is_copy_constructible_v<Operator>);
STATIC_REQUIRE_FALSE(std::is_copy_assignable_v<Operator>);
STATIC_REQUIRE_FALSE(std::is_move_constructible_v<Operator>);
STATIC_REQUIRE_FALSE(std::is_move_assignable_v<Operator>);
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);
Operator stellarOperator(f, *f.domainMapperStateless, barotrope, 1.13);
const mfem::Vector state = stellar_equilibrium_test_utils::make_state(
f, stellarOperator.GetLayout());
const auto dependencies = stellar_equilibrium_test_utils::make_dependencies();
const auto rotation = stellar_equilibrium_test_utils::make_rotation(0.81);
const auto report = stellarOperator.Prepare(state, dependencies, rotation);
CHECK(report.gravity.DidAnyWork());
CHECK(report.barotropicClosure.DidAnyWork());
CHECK(report.hydrostatic.DidAnyWork());
CHECK(report.displacement.DidAnyWork());
CHECK(report.massNormalization.DidAnyWork());
CHECK(report.assembledResidual);
CHECK(stellarOperator.IsPrepared());
CHECK(&stellarOperator.GetGravityOperator().GetLinearizationContext() ==
&stellarOperator.GetGravityContext());
CHECK(&stellarOperator.GetDisplacementOperator().GetGravityContext() ==
&stellarOperator.GetGravityContext());
CHECK(&stellarOperator.GetMassNormalizationOperator().GetGravityContext() ==
&stellarOperator.GetGravityContext());
CHECK(&stellarOperator.GetBarotropicClosureOperator().GetContext() ==
&stellarOperator.GetBarotropicClosureContext());
mfem::Vector coupledResidual;
stellarOperator.BuildResidual(coupledResidual);
const mfem::Vector expected =
stellar_equilibrium_test_utils::explicit_residual(stellarOperator, f,
state);
CHECK(stellar_equilibrium_test_utils::relative_difference(
coupledResidual, expected, f.mesh->GetComm()) < 2.0e-15);
CHECK(stellarOperator.Width() ==
stellarOperator.GetLayout().value_offsets().Last());
CHECK(stellarOperator.Height() ==
stellarOperator.GetLayout().residual_offsets().Last());
}
TEST_CASE("Prepared Stellar Equilibrium Has Exact Analytic Closure Hydrostatic "
"And Mass Rows",
tags::barotrope &tags::prepared &tags::analytic_comparison
&tags::accuracy) {
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(1.0, 0.25);
constexpr double enthalpy = 0.60;
const double density = barotrope.density_from_enthalpy(enthalpy);
constexpr double gravityPotential = 0.20;
constexpr double bernoulliConstant = enthalpy + gravityPotential;
const mean_field::mapping::COORDINATE_SPACE volumeCoordinates =
f.has_mapping() ? mean_field::mapping::COORDINATE_SPACE::PHYSICAL
: mean_field::mapping::COORDINATE_SPACE::REFERENCE;
const double targetMass =
density * mean_field::analysis::get_mesh_volume(
f, volumeCoordinates, mean_field::utils::DOMAINS::STELLAR);
mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator(
f, *f.domainMapperStateless, barotrope, targetMass);
const auto &layout = stellarOperator.GetLayout();
mfem::Vector state(layout.value_offsets().Last());
state = 0.0;
stellar_equilibrium_test_utils::assign_value_block(
state, layout, stellar_equilibrium_test_utils::densityValue,
stellar_equilibrium_test_utils::reduce_density(
f, stellar_equilibrium_test_utils::project_constant_density(
f, density)));
stellar_equilibrium_test_utils::assign_value_block(
state, layout, stellar_equilibrium_test_utils::gravityPotentialValue,
stellar_equilibrium_test_utils::project_constant_scalar(
*f.gravityPotentialFes, gravityPotential));
stellar_equilibrium_test_utils::assign_value_block(
state, layout, stellar_equilibrium_test_utils::enthalpyValue,
stellar_equilibrium_test_utils::reduce_enthalpy(
f, stellar_equilibrium_test_utils::project_constant_scalar(
*f.enthalpyFes, enthalpy)));
stellar_equilibrium_test_utils::value_view(
state, layout, stellar_equilibrium_test_utils::bernoulliValue)(0) =
bernoulliConstant;
stellarOperator.Prepare(state,
stellar_equilibrium_test_utils::make_dependencies(),
stellar_equilibrium_test_utils::make_zero_rotation());
mfem::Vector residual;
stellarOperator.BuildResidual(residual);
const double closureNorm = stellar_equilibrium_test_utils::global_norm(
stellar_equilibrium_test_utils::const_residual_view(
residual, layout, stellar_equilibrium_test_utils::densityResidual),
f.mesh->GetComm());
const double hydrostaticNorm = stellar_equilibrium_test_utils::global_norm(
stellar_equilibrium_test_utils::const_residual_view(
residual, layout, stellar_equilibrium_test_utils::enthalpyResidual),
f.mesh->GetComm());
const double massError =
std::abs(stellar_equilibrium_test_utils::const_residual_view(
residual, layout, stellar_equilibrium_test_utils::massResidual)(0));
INFO("Exact n=1 closure norm = " << closureNorm);
INFO("Exact constant hydrostatic norm = " << hydrostaticNorm);
INFO("Independent constant-density mass error = " << massError);
CHECK(closureNorm < 2.0e-12);
CHECK(hydrostaticNorm < 2.0e-12);
CHECK(massError < 2.0e-11 * targetMass);
}
TEST_CASE(
"Prepared Stellar Equilibrium Zero Gravity State Has Analytically Zero "
"Gravity Rows",
tags::gravity &tags::prepared &tags::analytic_comparison &tags::residuals) {
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);
mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator(
f, *f.domainMapperStateless, barotrope, 1.0);
const auto &layout = stellarOperator.GetLayout();
mfem::Vector state(layout.value_offsets().Last());
state = 0.0;
stellar_equilibrium_test_utils::assign_value_block(
state, layout, stellar_equilibrium_test_utils::displacementValue,
stellar_equilibrium_test_utils::project_displacement(f, 0.73));
stellarOperator.Prepare(state,
stellar_equilibrium_test_utils::make_dependencies(),
stellar_equilibrium_test_utils::make_zero_rotation());
mfem::Vector residual;
stellarOperator.BuildResidual(residual);
const double gradientNorm = stellar_equilibrium_test_utils::global_norm(
stellar_equilibrium_test_utils::const_residual_view(
residual, layout,
stellar_equilibrium_test_utils::gravityGradientResidual),
f.mesh->GetComm());
const double poissonNorm = stellar_equilibrium_test_utils::global_norm(
stellar_equilibrium_test_utils::const_residual_view(
residual, layout,
stellar_equilibrium_test_utils::gravityPotentialResidual),
f.mesh->GetComm());
CHECK(gradientNorm == 0.0);
CHECK(poissonNorm == 0.0);
}
TEST_CASE(
"Prepared Stellar Equilibrium Jacobian Has The Declared Six By Six Shape",
tags::barotrope &tags::prepared &tags::jacobian &tags::mfem_operators
&tags::unit) {
using JacobianForm =
mean_field::utils::blocks::barotropic_equilibrium_jacobian_form;
STATIC_REQUIRE(
mean_field::utils::blocks::has_jacobian_coupling_v<
mean_field::utils::blocks::barotropic_constant::mass_normalization::
residual,
mean_field::utils::blocks::density::mass::value, JacobianForm>);
STATIC_REQUIRE(mean_field::utils::blocks::has_jacobian_coupling_v<
mean_field::utils::blocks::barotropic_constant::
mass_normalization::residual,
mean_field::utils::blocks::displacement::geometry::value,
JacobianForm>);
STATIC_REQUIRE_FALSE(
mean_field::utils::blocks::has_jacobian_coupling_v<
mean_field::utils::blocks::barotropic_constant::mass_normalization::
residual,
mean_field::utils::blocks::enthalpy::specific::value, JacobianForm>);
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);
mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator(
f, *f.domainMapperStateless, barotrope, 1.17);
const auto &layout = stellarOperator.GetLayout();
const mfem::Vector state =
stellar_equilibrium_test_utils::make_state(f, layout);
stellarOperator.Prepare(state,
stellar_equilibrium_test_utils::make_dependencies(),
stellar_equilibrium_test_utils::make_rotation(0.77));
const mfem::Vector fullDirection =
stellar_equilibrium_test_utils::make_direction(f, layout);
struct ShapeCase final {
int activeColumn;
std::array<bool, 6> allowedRows;
};
const std::array<ShapeCase, 6> cases{
ShapeCase{0, {false, true, true, true, false, true}},
ShapeCase{1, {true, true, true, true, true, true}},
ShapeCase{2, {true, true, false, true, false, false}},
ShapeCase{3, {true, false, false, false, true, false}},
ShapeCase{4, {false, false, true, true, true, false}},
ShapeCase{5, {false, false, false, false, true, false}}};
const std::array<int, 7> valueOffsets{
layout.offset(stellar_equilibrium_test_utils::densityValue),
layout.offset(stellar_equilibrium_test_utils::displacementValue),
layout.offset(stellar_equilibrium_test_utils::gravityGradientValue),
layout.offset(stellar_equilibrium_test_utils::gravityPotentialValue),
layout.offset(stellar_equilibrium_test_utils::enthalpyValue),
layout.offset(stellar_equilibrium_test_utils::bernoulliValue),
layout.value_offsets().Last()};
for (const ShapeCase &shapeCase : cases) {
CAPTURE(shapeCase.activeColumn);
mfem::Vector columnDirection(fullDirection.Size());
columnDirection = 0.0;
for (int entry = valueOffsets[shapeCase.activeColumn];
entry < valueOffsets[shapeCase.activeColumn + 1]; ++entry) {
columnDirection(entry) = fullDirection(entry);
}
mfem::Vector action;
stellarOperator.Mult(columnDirection, action);
const std::array<mfem::Vector, 6> rowActions{
stellar_equilibrium_test_utils::const_residual_view(
action, layout,
stellar_equilibrium_test_utils::gravityGradientResidual),
stellar_equilibrium_test_utils::const_residual_view(
action, layout,
stellar_equilibrium_test_utils::gravityPotentialResidual),
stellar_equilibrium_test_utils::const_residual_view(
action, layout, stellar_equilibrium_test_utils::densityResidual),
stellar_equilibrium_test_utils::const_residual_view(
action, layout,
stellar_equilibrium_test_utils::displacementResidual),
stellar_equilibrium_test_utils::const_residual_view(
action, layout, stellar_equilibrium_test_utils::enthalpyResidual),
stellar_equilibrium_test_utils::const_residual_view(
action, layout, stellar_equilibrium_test_utils::massResidual)};
double allowedNormSquared = 0.0;
for (int row = 0; row < 6; ++row) {
CAPTURE(row);
const double rowNorm = stellar_equilibrium_test_utils::global_norm(
rowActions[row], f.mesh->GetComm());
if (shapeCase.allowedRows[row]) {
allowedNormSquared += rowNorm * rowNorm;
} else {
CHECK(rowNorm == 0.0);
}
}
CHECK(allowedNormSquared > 0.0);
}
}
TEST_CASE("Prepared Stellar Equilibrium Complete Jacobian Matches Every "
"Coupled Centered Difference Block",
tags::barotrope &tags::prepared &tags::jacobian &tags::accuracy
&tags::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);
REQUIRE(f.okay());
const mean_field::eos::Polytrope barotrope(3.0, 0.25);
mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator(
f, *f.domainMapperStateless, barotrope, 1.21);
const auto &layout = stellarOperator.GetLayout();
const mfem::Vector baseState =
stellar_equilibrium_test_utils::make_state(f, layout);
const mfem::Vector direction =
stellar_equilibrium_test_utils::make_direction(f, layout);
auto dependencies = stellar_equilibrium_test_utils::make_dependencies();
const auto rotation = stellar_equilibrium_test_utils::make_rotation(0.85);
stellarOperator.Prepare(baseState, dependencies, rotation);
mfem::Vector analyticAction;
stellarOperator.Mult(direction, analyticAction);
constexpr double step = 1.0e-5;
mfem::Vector plusState(baseState);
plusState.Add(step, direction);
stellar_equilibrium_test_utils::increment_all_state_revisions(dependencies);
stellarOperator.Prepare(plusState, dependencies, rotation);
mfem::Vector plusResidual;
stellarOperator.BuildResidual(plusResidual);
mfem::Vector minusState(baseState);
minusState.Add(-step, direction);
stellar_equilibrium_test_utils::increment_all_state_revisions(dependencies);
stellarOperator.Prepare(minusState, dependencies, rotation);
mfem::Vector minusResidual;
stellarOperator.BuildResidual(minusResidual);
plusResidual -= minusResidual;
plusResidual /= 2.0 * step;
const std::array<double, 6> errors{
stellar_equilibrium_test_utils::block_relative_difference(
analyticAction, plusResidual, layout,
stellar_equilibrium_test_utils::gravityGradientResidual,
f.mesh->GetComm()),
stellar_equilibrium_test_utils::block_relative_difference(
analyticAction, plusResidual, layout,
stellar_equilibrium_test_utils::gravityPotentialResidual,
f.mesh->GetComm()),
stellar_equilibrium_test_utils::block_relative_difference(
analyticAction, plusResidual, layout,
stellar_equilibrium_test_utils::densityResidual, f.mesh->GetComm()),
stellar_equilibrium_test_utils::block_relative_difference(
analyticAction, plusResidual, layout,
stellar_equilibrium_test_utils::displacementResidual,
f.mesh->GetComm()),
stellar_equilibrium_test_utils::block_relative_difference(
analyticAction, plusResidual, layout,
stellar_equilibrium_test_utils::enthalpyResidual, f.mesh->GetComm()),
stellar_equilibrium_test_utils::block_relative_difference(
analyticAction, plusResidual, layout,
stellar_equilibrium_test_utils::massResidual, f.mesh->GetComm())};
INFO("R_g centered-difference error = " << errors[0]);
INFO("R_Phi centered-difference error = " << errors[1]);
INFO("R_rho centered-difference error = " << errors[2]);
INFO("R_d centered-difference error = " << errors[3]);
INFO("R_h centered-difference error = " << errors[4]);
INFO("R_M centered-difference error = " << errors[5]);
CHECK(errors[0] < 2.0e-6);
CHECK(errors[1] < 2.0e-6);
CHECK(errors[2] < 2.0e-6);
CHECK(errors[3] < 2.0e-6);
CHECK(errors[4] < 2.0e-6);
CHECK(errors[5] < 2.0e-6);
}
TEST_CASE("Prepared Stellar Equilibrium Bernoulli Newton Step Decreases The "
"Residual Exactly",
tags::barotrope &tags::prepared &tags::jacobian &tags::convergence) {
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);
mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator(
f, *f.domainMapperStateless, barotrope, 1.09);
const auto &layout = stellarOperator.GetLayout();
mfem::Vector state = stellar_equilibrium_test_utils::make_state(f, layout);
auto dependencies = stellar_equilibrium_test_utils::make_dependencies();
const auto rotation = stellar_equilibrium_test_utils::make_rotation(0.69);
stellarOperator.Prepare(state, dependencies, rotation);
mfem::Vector residualBefore;
stellarOperator.BuildResidual(residualBefore);
mfem::Vector unitBernoulliDirection(state.Size());
unitBernoulliDirection = 0.0;
stellar_equilibrium_test_utils::value_view(
unitBernoulliDirection, layout,
stellar_equilibrium_test_utils::bernoulliValue)(0) = 1.0;
mfem::Vector bernoulliAction;
stellarOperator.Mult(unitBernoulliDirection, bernoulliAction);
const mfem::Vector residualHydrostatic =
stellar_equilibrium_test_utils::const_residual_view(
residualBefore, layout,
stellar_equilibrium_test_utils::enthalpyResidual);
const mfem::Vector actionHydrostatic =
stellar_equilibrium_test_utils::const_residual_view(
bernoulliAction, layout,
stellar_equilibrium_test_utils::enthalpyResidual);
const double numerator = gravity_prepared_test_utils::global_dot(
residualHydrostatic, actionHydrostatic, f.mesh->GetComm());
const double denominator = gravity_prepared_test_utils::global_dot(
actionHydrostatic, actionHydrostatic, f.mesh->GetComm());
REQUIRE(denominator > 0.0);
const double bernoulliStep = -numerator / denominator;
mfem::Vector predictedResidual(residualBefore);
predictedResidual.Add(bernoulliStep, bernoulliAction);
stellar_equilibrium_test_utils::value_view(
state, layout, stellar_equilibrium_test_utils::bernoulliValue)(0) +=
bernoulliStep;
++dependencies.bernoulliConstant.revision;
stellarOperator.Prepare(state, dependencies, rotation);
mfem::Vector residualAfter;
stellarOperator.BuildResidual(residualAfter);
const double modelError = stellar_equilibrium_test_utils::relative_difference(
residualAfter, predictedResidual, f.mesh->GetComm());
const double hydrostaticNormBefore =
stellar_equilibrium_test_utils::global_norm(residualHydrostatic,
f.mesh->GetComm());
const double hydrostaticNormAfter =
stellar_equilibrium_test_utils::global_norm(
stellar_equilibrium_test_utils::const_residual_view(
residualAfter, layout,
stellar_equilibrium_test_utils::enthalpyResidual),
f.mesh->GetComm());
const double coupledNormBefore = stellar_equilibrium_test_utils::global_norm(
residualBefore, f.mesh->GetComm());
const double coupledNormAfter = stellar_equilibrium_test_utils::global_norm(
residualAfter, f.mesh->GetComm());
INFO("Bernoulli least-squares step = " << bernoulliStep);
INFO("Exact affine residual-model error = " << modelError);
INFO("Hydrostatic norm before = " << hydrostaticNormBefore);
INFO("Hydrostatic norm after = " << hydrostaticNormAfter);
INFO("Coupled norm before = " << coupledNormBefore);
INFO("Coupled norm after = " << coupledNormAfter);
CHECK(modelError < 2.0e-13);
CHECK(hydrostaticNormAfter < hydrostaticNormBefore);
CHECK(coupledNormAfter <= coupledNormBefore);
mfem::Vector unchangedDifference(residualAfter);
unchangedDifference -= residualBefore;
stellar_equilibrium_test_utils::residual_view(
unchangedDifference, layout,
stellar_equilibrium_test_utils::enthalpyResidual) = 0.0;
CHECK(stellar_equilibrium_test_utils::global_norm(unchangedDifference,
f.mesh->GetComm()) == 0.0);
}
TEST_CASE(
"Prepared Stellar Equilibrium Selectively Invalidates Rows And Never "
"Reprepares In Krylov Mult",
tags::barotrope &tags::prepared &tags::contexts &tags::mfem_operators) {
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);
mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator(
f, *f.domainMapperStateless, barotrope, 1.15);
const auto &layout = stellarOperator.GetLayout();
mfem::Vector state = stellar_equilibrium_test_utils::make_state(f, layout);
auto dependencies = stellar_equilibrium_test_utils::make_dependencies();
const auto rotation = stellar_equilibrium_test_utils::make_rotation(0.73);
stellarOperator.Prepare(state, dependencies, rotation);
const std::uint64_t closurePreparations =
stellarOperator.GetBarotropicClosureOperator().GetPreparationCount();
const std::uint64_t hydrostaticPreparations =
stellarOperator.GetHydrostaticOperator().GetResidualPreparationCount();
const std::uint64_t displacementPreparations =
stellarOperator.GetDisplacementOperator().GetResidualPreparationCount();
const std::uint64_t massPreparations =
stellarOperator.GetMassNormalizationOperator().GetPreparationCount();
const std::uint64_t rootAssemblies =
stellarOperator.GetStatistics().residualAssemblies;
const auto repeated = stellarOperator.Prepare(state, dependencies, rotation);
CHECK_FALSE(repeated.DidAnyWork());
CHECK(stellarOperator.GetStatistics().residualAssemblies == rootAssemblies);
const mfem::Vector direction =
stellar_equilibrium_test_utils::make_direction(f, layout);
mfem::Vector action;
stellarOperator.Mult(direction, action);
stellarOperator.Mult(direction, action);
stellarOperator.Mult(direction, action);
CHECK(stellarOperator.GetBarotropicClosureOperator().GetPreparationCount() ==
closurePreparations);
CHECK(
stellarOperator.GetHydrostaticOperator().GetResidualPreparationCount() ==
hydrostaticPreparations);
CHECK(
stellarOperator.GetDisplacementOperator().GetResidualPreparationCount() ==
displacementPreparations);
CHECK(stellarOperator.GetMassNormalizationOperator().GetPreparationCount() ==
massPreparations);
CHECK(stellarOperator.GetStatistics().residualAssemblies == rootAssemblies);
CHECK(stellarOperator.GetStatistics().jacobianApplications == 3);
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));
++dependencies.gravityPotential.revision;
const auto potentialReport =
stellarOperator.Prepare(state, dependencies, rotation);
CHECK_FALSE(potentialReport.barotropicClosure.DidAnyWork());
CHECK(potentialReport.hydrostatic.DidAnyWork());
CHECK_FALSE(potentialReport.displacement.DidAnyWork());
CHECK_FALSE(potentialReport.massNormalization.DidAnyWork());
CHECK(potentialReport.assembledResidual);
stellar_equilibrium_test_utils::value_view(
state, layout, stellar_equilibrium_test_utils::bernoulliValue)(0) += 0.09;
++dependencies.bernoulliConstant.revision;
const auto bernoulliReport =
stellarOperator.Prepare(state, dependencies, rotation);
CHECK_FALSE(bernoulliReport.barotropicClosure.DidAnyWork());
CHECK(bernoulliReport.hydrostatic.DidAnyWork());
CHECK_FALSE(bernoulliReport.displacement.DidAnyWork());
CHECK_FALSE(bernoulliReport.massNormalization.DidAnyWork());
CHECK(bernoulliReport.assembledResidual);
++dependencies.targetMass.revision;
const auto targetReport =
stellarOperator.Prepare(state, dependencies, rotation);
CHECK_FALSE(targetReport.barotropicClosure.DidAnyWork());
CHECK_FALSE(targetReport.hydrostatic.DidAnyWork());
CHECK_FALSE(targetReport.displacement.DidAnyWork());
CHECK(targetReport.massNormalization.DidAnyWork());
CHECK(targetReport.assembledResidual);
}
TEST_CASE("Prepared Stellar Equilibrium Matches The Analytic N1 Lane Emden "
"State Up To The Mixed Projection Floor",
tags::barotrope &tags::prepared &tags::analytic_comparison
&tags::accuracy &tags::gravity &tags::hydro &tags::residuals) {
class LaneEmdenGravityGradientCoefficient final
: public mfem::VectorCoefficient {
public:
LaneEmdenGravityGradientCoefficient(const int dimension,
const int vacuumAttribute,
const double stellarRadius,
const double centralDensity,
const double targetMass,
const double polytropicConstant)
: mfem::VectorCoefficient(dimension),
m_vacuumAttribute(vacuumAttribute), m_stellarRadius(stellarRadius),
m_centralDensity(centralDensity), m_targetMass(targetMass),
m_polytropicConstant(polytropicConstant) {}
void Eval(mfem::Vector &value, mfem::ElementTransformation &transformation,
const mfem::IntegrationPoint &integrationPoint) override {
mfem::Vector computationalPosition;
transformation.Transform(integrationPoint, computationalPosition);
value.SetSize(vdim);
value = 0.0;
const double radius = computationalPosition.Norml2();
if (!std::isfinite(radius) ||
radius <= 100.0 * std::numeric_limits<double>::epsilon()) {
return;
}
double radialGradient = 0.0;
if (transformation.Attribute == m_vacuumAttribute) {
/*
* In the compactified exterior, the three-dimensional H(div)
* Piola pullback of the inverse-square monopole field reduces
* to this finite computational-space expression.
*/
radialGradient =
mean_field::utils::G * m_targetMass / (radius * radius);
} else {
const double pi = std::acos(-1.0);
const double xi = pi * radius / m_stellarRadius;
if (std::abs(xi) < 1.0e-5) {
/*
* sin(xi) - xi cos(xi) = xi^3 / 3 + O(xi^5).
*/
radialGradient = (4.0 / 3.0) * pi * mean_field::utils::G *
m_centralDensity * radius;
} else {
radialGradient = 2.0 * m_polytropicConstant * m_centralDensity * pi /
m_stellarRadius *
(std::sin(xi) - xi * std::cos(xi)) / (xi * xi);
}
}
value = computationalPosition;
value *= radialGradient / radius;
}
private:
int m_vacuumAttribute;
double m_stellarRadius;
double m_centralDensity;
double m_targetMass;
double m_polytropicConstant;
};
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());
REQUIRE(f.domainMapperStateless != nullptr);
REQUIRE(f.domainMapperStateless != nullptr);
*f.displacement = 0.0;
const double pi = std::acos(-1.0);
const double stellarRadius = mean_field::utils::RADIUS;
const double targetMass = mean_field::utils::MASS;
/*
* For an n = 1 Lane-Emden polytrope,
*
* R = sqrt(pi K / (2 G)),
*
* so choosing K this way places the analytic surface exactly at the
* stellar boundary of the mesh.
*/
const double polytropicConstant =
2.0 * mean_field::utils::G * stellarRadius * stellarRadius / pi;
/*
* The analytic n = 1 mass is
*
* M = 4 rho_c R^3 / pi.
*/
const double centralDensity =
pi * targetMass / (4.0 * stellarRadius * stellarRadius * stellarRadius);
const double bernoulliConstant =
-mean_field::utils::G * targetMass / stellarRadius;
const mean_field::eos::Polytrope barotrope(1.0, polytropicConstant);
const auto densityFunction = [centralDensity, stellarRadius,
pi](const mfem::Vector &position) {
const double radius = position.Norml2();
if (radius >= stellarRadius) {
return 0.0;
}
const double xi = pi * radius / stellarRadius;
if (std::abs(xi) < 100.0 * std::numeric_limits<double>::epsilon()) {
return centralDensity;
}
return centralDensity * std::sin(xi) / xi;
};
const auto enthalpyFunction = [centralDensity, stellarRadius,
polytropicConstant,
pi](const mfem::Vector &position) {
const double radius = position.Norml2();
if (radius >= stellarRadius) {
return 0.0;
}
const double xi = pi * radius / stellarRadius;
const double density =
std::abs(xi) < 100.0 * std::numeric_limits<double>::epsilon()
? centralDensity
: centralDensity * std::sin(xi) / xi;
return 2.0 * polytropicConstant * density;
};
const auto potentialFunction = [centralDensity, stellarRadius, targetMass,
polytropicConstant, bernoulliConstant,
pi](const mfem::Vector &physicalPosition) {
const double radius = physicalPosition.Norml2();
/*
* Phi tends to zero at compactified infinity.
*/
if (!std::isfinite(radius)) {
return 0.0;
}
if (radius >= stellarRadius) {
return radius > 0.0 ? -mean_field::utils::G * targetMass / radius : 0.0;
}
const double xi = pi * radius / stellarRadius;
const double density =
std::abs(xi) < 100.0 * std::numeric_limits<double>::epsilon()
? centralDensity
: centralDensity * std::sin(xi) / xi;
const double enthalpy = 2.0 * polytropicConstant * density;
/*
* Hydrostatic equilibrium is h + Phi = C.
*/
return bernoulliConstant - enthalpy;
};
mfem::FunctionCoefficient densityCoefficient(densityFunction);
mfem::FunctionCoefficient enthalpyCoefficient(enthalpyFunction);
mean_field::mapping::PhysicalPositionFunctionCoefficient potentialCoefficient(
*f.domainMapperStateless, *f.displacement, *f.compactificationCoordinate,
potentialFunction);
LaneEmdenGravityGradientCoefficient gravityGradientCoefficient(
f.mesh->Dimension(), field_dof_test_utils::vacuum_material_attribute,
stellarRadius, centralDensity, targetMass, polytropicConstant);
mfem::ParGridFunction densityField(f.densityFes.get());
mfem::ParGridFunction enthalpyField(f.enthalpyFes.get());
mfem::ParGridFunction gravityPotentialField(f.gravityPotentialFes.get());
mfem::ParGridFunction gravityGradientField(f.gravityFluxFes.get());
densityField = 0.0;
enthalpyField = 0.0;
gravityPotentialField = 0.0;
gravityGradientField = 0.0;
densityField.ProjectCoefficient(densityCoefficient);
enthalpyField.ProjectCoefficient(enthalpyCoefficient);
gravityPotentialField.ProjectCoefficient(potentialCoefficient);
gravityGradientField.ProjectCoefficient(gravityGradientCoefficient);
mfem::Vector densityTrue;
mfem::Vector enthalpyTrue;
mfem::Vector gravityPotentialTrue;
mfem::Vector gravityGradientTrue;
densityField.GetTrueDofs(densityTrue);
enthalpyField.GetTrueDofs(enthalpyTrue);
gravityPotentialField.GetTrueDofs(gravityPotentialTrue);
gravityGradientField.GetTrueDofs(gravityGradientTrue);
mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator(
f, *f.domainMapperStateless, barotrope, targetMass);
const mean_field::operators::StellarEquilibriumLayout &layout =
stellarOperator.GetLayout();
mfem::Vector analyticState(layout.value_offsets().Last());
analyticState = 0.0;
stellar_equilibrium_test_utils::assign_value_block(
analyticState, layout, stellar_equilibrium_test_utils::densityValue,
stellar_equilibrium_test_utils::reduce_density(f, densityTrue));
stellar_equilibrium_test_utils::assign_value_block(
analyticState, layout,
stellar_equilibrium_test_utils::gravityGradientValue,
gravityGradientTrue);
stellar_equilibrium_test_utils::assign_value_block(
analyticState, layout,
stellar_equilibrium_test_utils::gravityPotentialValue,
gravityPotentialTrue);
stellar_equilibrium_test_utils::assign_value_block(
analyticState, layout, stellar_equilibrium_test_utils::enthalpyValue,
stellar_equilibrium_test_utils::reduce_enthalpy(f, enthalpyTrue));
stellar_equilibrium_test_utils::value_view(
analyticState, layout,
stellar_equilibrium_test_utils::bernoulliValue)(0) = bernoulliConstant;
mean_field::operators::StellarEquilibriumDependencies dependencies =
stellar_equilibrium_test_utils::make_dependencies();
const mean_field::physics::RigidRotation zeroRotation =
stellar_equilibrium_test_utils::make_zero_rotation();
stellarOperator.Prepare(analyticState, dependencies, zeroRotation);
mfem::Vector analyticResidual;
stellarOperator.BuildResidual(analyticResidual);
const double analyticGradientNorm =
stellar_equilibrium_test_utils::global_norm(
stellar_equilibrium_test_utils::const_residual_view(
analyticResidual, layout,
stellar_equilibrium_test_utils::gravityGradientResidual),
f.mesh->GetComm());
const double analyticPoissonNorm =
stellar_equilibrium_test_utils::global_norm(
stellar_equilibrium_test_utils::const_residual_view(
analyticResidual, layout,
stellar_equilibrium_test_utils::gravityPotentialResidual),
f.mesh->GetComm());
const double analyticClosureNorm =
stellar_equilibrium_test_utils::global_norm(
stellar_equilibrium_test_utils::const_residual_view(
analyticResidual, layout,
stellar_equilibrium_test_utils::densityResidual),
f.mesh->GetComm());
const double analyticDisplacementNorm =
stellar_equilibrium_test_utils::global_norm(
stellar_equilibrium_test_utils::const_residual_view(
analyticResidual, layout,
stellar_equilibrium_test_utils::displacementResidual),
f.mesh->GetComm());
const double analyticHydrostaticNorm =
stellar_equilibrium_test_utils::global_norm(
stellar_equilibrium_test_utils::const_residual_view(
analyticResidual, layout,
stellar_equilibrium_test_utils::enthalpyResidual),
f.mesh->GetComm());
const double analyticMassError =
std::abs(stellar_equilibrium_test_utils::const_residual_view(
analyticResidual, layout,
stellar_equilibrium_test_utils::massResidual)(0));
/*
* Construct a deliberately inconsistent nearby state. The analytic
* projection should have a substantially smaller residual in every row.
*/
mfem::Vector perturbedState(analyticState);
{
mfem::Vector block = stellar_equilibrium_test_utils::value_view(
perturbedState, layout, stellar_equilibrium_test_utils::densityValue);
block *= 1.12;
}
{
mfem::Vector block = stellar_equilibrium_test_utils::value_view(
perturbedState, layout,
stellar_equilibrium_test_utils::gravityGradientValue);
block *= 0.87;
}
{
mfem::Vector block = stellar_equilibrium_test_utils::value_view(
perturbedState, layout,
stellar_equilibrium_test_utils::gravityPotentialValue);
block *= 1.08;
}
{
mfem::Vector block = stellar_equilibrium_test_utils::value_view(
perturbedState, layout, stellar_equilibrium_test_utils::enthalpyValue);
block *= 0.91;
}
stellar_equilibrium_test_utils::value_view(
perturbedState, layout,
stellar_equilibrium_test_utils::bernoulliValue)(0) *= 1.04;
stellar_equilibrium_test_utils::increment_all_state_revisions(dependencies);
stellarOperator.Prepare(perturbedState, dependencies, zeroRotation);
mfem::Vector perturbedResidual;
stellarOperator.BuildResidual(perturbedResidual);
const double perturbedGradientNorm =
stellar_equilibrium_test_utils::global_norm(
stellar_equilibrium_test_utils::const_residual_view(
perturbedResidual, layout,
stellar_equilibrium_test_utils::gravityGradientResidual),
f.mesh->GetComm());
const double perturbedPoissonNorm =
stellar_equilibrium_test_utils::global_norm(
stellar_equilibrium_test_utils::const_residual_view(
perturbedResidual, layout,
stellar_equilibrium_test_utils::gravityPotentialResidual),
f.mesh->GetComm());
const double perturbedClosureNorm =
stellar_equilibrium_test_utils::global_norm(
stellar_equilibrium_test_utils::const_residual_view(
perturbedResidual, layout,
stellar_equilibrium_test_utils::densityResidual),
f.mesh->GetComm());
const double perturbedDisplacementNorm =
stellar_equilibrium_test_utils::global_norm(
stellar_equilibrium_test_utils::const_residual_view(
perturbedResidual, layout,
stellar_equilibrium_test_utils::displacementResidual),
f.mesh->GetComm());
const double perturbedHydrostaticNorm =
stellar_equilibrium_test_utils::global_norm(
stellar_equilibrium_test_utils::const_residual_view(
perturbedResidual, layout,
stellar_equilibrium_test_utils::enthalpyResidual),
f.mesh->GetComm());
const double perturbedMassError =
std::abs(stellar_equilibrium_test_utils::const_residual_view(
perturbedResidual, layout,
stellar_equilibrium_test_utils::massResidual)(0));
INFO("Analytic n=1 central density = " << centralDensity);
INFO("Analytic n=1 polytropic constant = " << polytropicConstant);
INFO("Analytic n=1 target mass = " << targetMass);
INFO("Analytic n=1 Bernoulli constant = " << bernoulliConstant);
INFO("Gravity-gradient residual: analytic = "
<< analyticGradientNorm << ", perturbed = " << perturbedGradientNorm);
INFO("Poisson residual: analytic = "
<< analyticPoissonNorm << ", perturbed = " << perturbedPoissonNorm);
INFO("Closure residual: analytic = "
<< analyticClosureNorm << ", perturbed = " << perturbedClosureNorm);
INFO("Displacement residual: analytic = " << analyticDisplacementNorm
<< ", perturbed = "
<< perturbedDisplacementNorm);
INFO("Hydrostatic residual: analytic = " << analyticHydrostaticNorm
<< ", perturbed = "
<< perturbedHydrostaticNorm);
INFO("Mass error: analytic = " << analyticMassError
<< ", perturbed = " << perturbedMassError);
REQUIRE(std::isfinite(perturbedGradientNorm));
REQUIRE(perturbedPoissonNorm > 0.0);
REQUIRE(perturbedClosureNorm > 0.0);
REQUIRE(perturbedDisplacementNorm > 0.0);
REQUIRE(perturbedHydrostaticNorm > 0.0);
REQUIRE(perturbedMassError > 0.0);
/*
* Closure and hydrostatic balance are algebraically especially favorable
* for n = 1 because h = 2 K rho and h + Phi = C are linear relations.
*/
CHECK(analyticClosureNorm < 0.10 * perturbedClosureNorm);
CHECK(analyticHydrostaticNorm < 0.10 * perturbedHydrostaticNorm);
/*
* Phi_h and g_h are independent L2 and RT projections of the analytic
* potential and gradient. They are not a commuting mixed projection and
* therefore need not satisfy
*
* M_g g_h + B^T Phi_h = 0.
*
* The resulting R_g value is a finite-element projection-compatibility
* floor, not a physical equilibrium error. Gravity solver-to-projection
* accuracy is tested independently by the dedicated gravity tests.
*/
CHECK(std::isfinite(analyticGradientNorm));
CHECK(analyticPoissonNorm < 0.35 * perturbedPoissonNorm);
CHECK(analyticDisplacementNorm < 0.35 * perturbedDisplacementNorm);
CHECK(analyticMassError < 5.0e-5 * targetMass);
CHECK(analyticMassError < 0.10 * perturbedMassError);
}
TEST_CASE(
"Prepared Stellar Equilibrium Has A Restoring Jacobian Around An N3 "
"Polytrope",
tags::barotrope &tags::prepared &tags::analytic_comparison &tags::accuracy
&tags::gravity &tags::hydro &tags::jacobian &tags::convergence) {
mean_field::utils::Args args = test_utils::setup_args();
args.p.rtol = 1.0e-12;
args.p.max_iters = std::max(args.p.max_iters, 1000);
mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.okay());
REQUIRE(f.domainMapperStateless != nullptr);
REQUIRE(f.domainMapperStateless != nullptr);
const double pi = std::acos(-1.0);
const double stellarRadius = mean_field::utils::RADIUS;
const double targetMass = mean_field::utils::MASS;
/*
* Standard n = 3 Lane-Emden constants:
*
* xi_1 = 6.896848619...
* -xi_1^2 theta'(xi_1) = 2.018235951...
*/
constexpr double surfaceCoordinate = 6.8968486193769603755;
constexpr double dimensionlessMass = 2.0182359509662283534;
/*
* For n = 3,
*
* M = 4 pi (K / (pi G))^(3/2) mu_1.
*
* This fixes K for the requested target mass.
*/
const double polytropicConstant =
pi * mean_field::utils::G *
std::pow(targetMass / (4.0 * pi * dimensionlessMass), 2.0 / 3.0);
/*
* The n = 3 radius is
*
* R = xi_1 sqrt(K / (pi G)) rho_c^(-1/3).
*
* Choose rho_c so that the Lane-Emden surface coincides with the
* stellar boundary of the test mesh.
*/
const double centralDensity =
std::pow(surfaceCoordinate *
std::sqrt(polytropicConstant / (pi * mean_field::utils::G)) /
stellarRadius,
3.0);
const mean_field::eos::Polytrope equationOfState(3.0, polytropicConstant);
const mean_field::models::structure::PolytropicStructure
structurePrescription(equationOfState, targetMass);
const mean_field::models::structure::StructureSeed seed =
structurePrescription.makeInitialSeed(
{.centralDensity = centralDensity, .radialSampleCount = 8192});
INFO("Requested stellar radius = " << stellarRadius);
INFO("Seed stellar radius = " << seed.stellarRadius);
INFO("Target mass = " << targetMass);
INFO("Polytropic constant = " << polytropicConstant);
INFO("Central density = " << centralDensity);
REQUIRE(seed.radius.Size() == seed.density.Size());
REQUIRE(seed.radius.Size() == seed.enthalpy.Size());
REQUIRE(seed.radius.Size() == 8192);
CHECK(std::abs(seed.stellarRadius - stellarRadius) / stellarRadius < 2.0e-4);
const auto interpolateProfile = [](const mfem::Vector &radiusSamples,
const mfem::Vector &valueSamples,
const double radius) {
MFEM_VERIFY(radiusSamples.Size() == valueSamples.Size(),
"The radial profile has inconsistent sample sizes.");
MFEM_VERIFY(radiusSamples.Size() >= 2,
"The radial profile requires at least two samples.");
if (radius <= radiusSamples(0)) {
return valueSamples(0);
}
const int finalIndex = radiusSamples.Size() - 1;
if (radius >= radiusSamples(finalIndex)) {
return valueSamples(finalIndex);
}
int lowerIndex = 0;
int upperIndex = finalIndex;
while (upperIndex - lowerIndex > 1) {
const int middleIndex = lowerIndex + (upperIndex - lowerIndex) / 2;
if (radiusSamples(middleIndex) <= radius) {
lowerIndex = middleIndex;
} else {
upperIndex = middleIndex;
}
}
const double radialInterval =
radiusSamples(upperIndex) - radiusSamples(lowerIndex);
MFEM_VERIFY(radialInterval > 0.0,
"The radial profile is not strictly increasing.");
const double fraction =
(radius - radiusSamples(lowerIndex)) / radialInterval;
return (1.0 - fraction) * valueSamples(lowerIndex) +
fraction * valueSamples(upperIndex);
};
mfem::FunctionCoefficient densityCoefficient(
[&seed, &interpolateProfile](const mfem::Vector &position) {
const double radius = position.Norml2();
if (radius >= seed.stellarRadius) {
return 0.0;
}
return interpolateProfile(seed.radius, seed.density, radius);
});
mfem::FunctionCoefficient enthalpyCoefficient(
[&seed, &interpolateProfile](const mfem::Vector &position) {
const double radius = position.Norml2();
if (radius >= seed.stellarRadius) {
return 0.0;
}
return interpolateProfile(seed.radius, seed.enthalpy, radius);
});
mfem::ParGridFunction densityField(f.densityFes.get());
mfem::ParGridFunction enthalpyField(f.enthalpyFes.get());
mfem::ParGridFunction displacementField(f.displacementFes.get());
densityField = 0.0;
enthalpyField = 0.0;
displacementField = 0.0;
densityField.ProjectCoefficient(densityCoefficient);
enthalpyField.ProjectCoefficient(enthalpyCoefficient);
/*
* Gravity initialization and the prepared root operator must see the
* same undeformed geometry.
*/
*f.displacement = displacementField;
const mean_field::physics::GravitySolution gravitySolution =
mean_field::physics::solve_gravity_field(f, args, densityField,
displacementField);
mfem::Vector densityTrue;
mfem::Vector enthalpyTrue;
mfem::Vector displacementTrue;
mfem::Vector gravityGradientTrue;
mfem::Vector gravityPotentialTrue;
densityField.GetTrueDofs(densityTrue);
enthalpyField.GetTrueDofs(enthalpyTrue);
displacementField.GetTrueDofs(displacementTrue);
gravitySolution.gradPhi.GetTrueDofs(gravityGradientTrue);
gravitySolution.phi.GetTrueDofs(gravityPotentialTrue);
const double bernoulliConstant =
-mean_field::utils::G * targetMass / stellarRadius;
const mean_field::eos::Polytrope barotrope(3.0, polytropicConstant);
mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator(
f, *f.domainMapperStateless, barotrope, targetMass);
const mean_field::operators::StellarEquilibriumLayout &layout =
stellarOperator.GetLayout();
mfem::Vector equilibriumState(layout.value_offsets().Last());
equilibriumState = 0.0;
stellar_equilibrium_test_utils::assign_value_block(
equilibriumState, layout, stellar_equilibrium_test_utils::densityValue,
stellar_equilibrium_test_utils::reduce_density(f, densityTrue));
stellar_equilibrium_test_utils::assign_value_block(
equilibriumState, layout,
stellar_equilibrium_test_utils::displacementValue, displacementTrue);
stellar_equilibrium_test_utils::assign_value_block(
equilibriumState, layout,
stellar_equilibrium_test_utils::gravityGradientValue,
gravityGradientTrue);
stellar_equilibrium_test_utils::assign_value_block(
equilibriumState, layout,
stellar_equilibrium_test_utils::gravityPotentialValue,
gravityPotentialTrue);
stellar_equilibrium_test_utils::assign_value_block(
equilibriumState, layout, stellar_equilibrium_test_utils::enthalpyValue,
stellar_equilibrium_test_utils::reduce_enthalpy(f, enthalpyTrue));
stellar_equilibrium_test_utils::value_view(
equilibriumState, layout,
stellar_equilibrium_test_utils::bernoulliValue)(0) = bernoulliConstant;
mean_field::operators::StellarEquilibriumDependencies dependencies =
stellar_equilibrium_test_utils::make_dependencies();
const mean_field::physics::RigidRotation zeroRotation =
stellar_equilibrium_test_utils::make_zero_rotation();
stellarOperator.Prepare(equilibriumState, dependencies, zeroRotation);
mfem::Vector equilibriumResidual;
stellarOperator.BuildResidual(equilibriumResidual);
/*
* Construct a physically safe perturbation direction. Density and
* enthalpy perturbations vanish at the surface because they are
* proportional to the equilibrium profiles.
*/
mfem::Vector perturbationDirection(layout.value_offsets().Last());
perturbationDirection = 0.0;
mfem::Vector densityDirection(densityTrue);
densityDirection *= 0.12;
mfem::Vector gravityGradientDirection(gravityGradientTrue);
gravityGradientDirection *= -0.09;
mfem::Vector gravityPotentialDirection(gravityPotentialTrue);
gravityPotentialDirection *= 0.07;
mfem::Vector enthalpyDirection(enthalpyTrue);
enthalpyDirection *= -0.11;
const mfem::Vector displacementDirection =
stellar_equilibrium_test_utils::project_displacement_direction(f, 0.15);
stellar_equilibrium_test_utils::assign_value_block(
perturbationDirection, layout,
stellar_equilibrium_test_utils::densityValue,
stellar_equilibrium_test_utils::reduce_density(f, densityDirection));
stellar_equilibrium_test_utils::assign_value_block(
perturbationDirection, layout,
stellar_equilibrium_test_utils::displacementValue, displacementDirection);
stellar_equilibrium_test_utils::assign_value_block(
perturbationDirection, layout,
stellar_equilibrium_test_utils::gravityGradientValue,
gravityGradientDirection);
stellar_equilibrium_test_utils::assign_value_block(
perturbationDirection, layout,
stellar_equilibrium_test_utils::gravityPotentialValue,
gravityPotentialDirection);
stellar_equilibrium_test_utils::assign_value_block(
perturbationDirection, layout,
stellar_equilibrium_test_utils::enthalpyValue,
stellar_equilibrium_test_utils::reduce_enthalpy(f, enthalpyDirection));
stellar_equilibrium_test_utils::value_view(
perturbationDirection, layout,
stellar_equilibrium_test_utils::bernoulliValue)(0) =
0.05 * bernoulliConstant;
/*
* Evaluate J delta-x at the equilibrium state before changing the
* prepared base point.
*/
mfem::Vector jacobianAction;
stellarOperator.Mult(perturbationDirection, jacobianAction);
constexpr double perturbationScale = 2.0e-2;
mfem::Vector perturbedState(equilibriumState);
perturbedState.Add(perturbationScale, perturbationDirection);
stellar_equilibrium_test_utils::increment_all_state_revisions(dependencies);
stellarOperator.Prepare(perturbedState, dependencies, zeroRotation);
mfem::Vector perturbedResidual;
stellarOperator.BuildResidual(perturbedResidual);
const auto residualBlockNorm = [&layout, &f](const mfem::Vector &residual,
const auto block) {
return stellar_equilibrium_test_utils::global_norm(
stellar_equilibrium_test_utils::const_residual_view(residual, layout,
block),
f.mesh->GetComm());
};
const std::array<double, 6> equilibriumRowNorms{
residualBlockNorm(
equilibriumResidual,
stellar_equilibrium_test_utils::gravityGradientResidual),
residualBlockNorm(
equilibriumResidual,
stellar_equilibrium_test_utils::gravityPotentialResidual),
residualBlockNorm(equilibriumResidual,
stellar_equilibrium_test_utils::densityResidual),
residualBlockNorm(equilibriumResidual,
stellar_equilibrium_test_utils::displacementResidual),
residualBlockNorm(equilibriumResidual,
stellar_equilibrium_test_utils::enthalpyResidual),
residualBlockNorm(equilibriumResidual,
stellar_equilibrium_test_utils::massResidual)};
const std::array<double, 6> perturbedRowNorms{
residualBlockNorm(
perturbedResidual,
stellar_equilibrium_test_utils::gravityGradientResidual),
residualBlockNorm(
perturbedResidual,
stellar_equilibrium_test_utils::gravityPotentialResidual),
residualBlockNorm(perturbedResidual,
stellar_equilibrium_test_utils::densityResidual),
residualBlockNorm(perturbedResidual,
stellar_equilibrium_test_utils::displacementResidual),
residualBlockNorm(perturbedResidual,
stellar_equilibrium_test_utils::enthalpyResidual),
residualBlockNorm(perturbedResidual,
stellar_equilibrium_test_utils::massResidual)};
constexpr std::array<const char *, 6> rowNames{
"gravity-gradient", "Poisson", "closure",
"displacement", "hydrostatic", "mass"};
/*
* Most rows are close to exact discrete relations. The displacement row
* combines independently projected thermodynamic fields with the discrete
* gravity solution and consequently has a larger force-balance projection
* floor.
*/
constexpr std::array<double, 6> maximumEquilibriumFractions{
0.35, // gravity-gradient
0.35, // Poisson
0.35, // closure
0.60, // displacement-force balance
0.35, // hydrostatic
0.35 // mass
};
for (int row = 0; row < 6; ++row) {
CAPTURE(row);
CAPTURE(rowNames[row]);
CAPTURE(equilibriumRowNorms[row]);
CAPTURE(perturbedRowNorms[row]);
CAPTURE(maximumEquilibriumFractions[row]);
REQUIRE(std::isfinite(equilibriumRowNorms[row]));
REQUIRE(std::isfinite(perturbedRowNorms[row]));
REQUIRE(perturbedRowNorms[row] > 0.0);
/*
* The Lane-Emden state must be closer to equilibrium than the nearby
* perturbed state in every residual row.
*/
CHECK(equilibriumRowNorms[row] < perturbedRowNorms[row]);
/*
* Require a substantial separation from the perturbed state while
* allowing the larger discrete projection floor in the force row.
*/
CHECK(equilibriumRowNorms[row] <
maximumEquilibriumFractions[row] * perturbedRowNorms[row]);
}
/*
* Record an absolute regression bound for the current coarse-mesh
* displacement-force projection floor.
*/
CHECK(equilibriumRowNorms[3] < 1.0e-3);
const double equilibriumMassError =
std::abs(stellar_equilibrium_test_utils::const_residual_view(
equilibriumResidual, layout,
stellar_equilibrium_test_utils::massResidual)(0));
INFO("Equilibrium relative mass error = " << equilibriumMassError /
targetMass);
CHECK(equilibriumMassError < 5.0e-4 * targetMass);
/*
* The nonlinear residual departure should be
*
* R(x + epsilon p) - R(x)
* = epsilon J(x) p + O(epsilon^2).
*/
mfem::Vector residualDeparture(perturbedResidual);
residualDeparture -= equilibriumResidual;
mfem::Vector linearizedDeparture(jacobianAction);
linearizedDeparture *= perturbationScale;
mfem::Vector nonlinearRemainder(residualDeparture);
nonlinearRemainder -= linearizedDeparture;
const double departureNorm = stellar_equilibrium_test_utils::global_norm(
residualDeparture, f.mesh->GetComm());
const double nonlinearRemainderNorm =
stellar_equilibrium_test_utils::global_norm(nonlinearRemainder,
f.mesh->GetComm());
/*
* Apply the known restoring correction -epsilon p through the Jacobian.
*
* This predicts the residual after returning to the equilibrium state:
*
* R(x + epsilon p) - epsilon J(x)p approximately R(x).
*/
mfem::Vector restoredResidualPrediction(perturbedResidual);
restoredResidualPrediction.Add(-perturbationScale, jacobianAction);
restoredResidualPrediction -= equilibriumResidual;
const double restoredDistance = stellar_equilibrium_test_utils::global_norm(
restoredResidualPrediction, f.mesh->GetComm());
INFO("Residual departure norm = " << departureNorm);
INFO("Nonlinear remainder norm = " << nonlinearRemainderNorm);
INFO("Distance after the restoring Jacobian correction = "
<< restoredDistance);
INFO("Relative first-order remainder = " << nonlinearRemainderNorm /
departureNorm);
REQUIRE(std::isfinite(departureNorm));
REQUIRE(std::isfinite(nonlinearRemainderNorm));
REQUIRE(std::isfinite(restoredDistance));
REQUIRE(departureNorm > 0.0);
CHECK(nonlinearRemainderNorm < 5.0e-2 * departureNorm);
CHECK(restoredDistance < 5.0e-2 * departureNorm);
/*
* Rotational shape response
*
* At moderate rotation, the leading deformation is a smooth, axisymmetric,
* approximately quadrupolar oblateness. A convenient volume-preserving
* affine representative is
*
* delta d(X) = (X, Y, -2 Z).
*
* It moves the equator outward, moves the poles inward, and has zero trace.
* A cusp is not expected until the nonlinear solution approaches mass
* shedding.
*/
{
const double keplerianAngularSpeed =
std::sqrt(mean_field::utils::G * targetMass /
(stellarRadius * stellarRadius * stellarRadius));
constexpr double rotationFraction = 0.50;
const double angularSpeed = rotationFraction * 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);
/*
* Return from the perturbed state used by the preceding Jacobian test to
* the spherical equilibrium state, while changing the rotation stream.
*/
stellar_equilibrium_test_utils::increment_all_state_revisions(dependencies);
++dependencies.rotation.revision;
stellarOperator.Prepare(equilibriumState, dependencies, rotation);
mfem::Vector rotatingSphericalResidual;
stellarOperator.BuildResidual(rotatingSphericalResidual);
mfem::ParGridFunction oblateDisplacementField(f.displacementFes.get());
mfem::VectorFunctionCoefficient oblateDisplacementCoefficient(
f.mesh->Dimension(),
[](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(3);
/*
* Positive amplitude:
*
* equator: d = (x, y, 0), outward
* pole: d = (0, 0, -2 z), inward
*
* The displacement gradient has trace 1 + 1 - 2 = 0, so this is
* volume preserving to first order.
*/
value(0) = position(0);
value(1) = position(1);
value(2) = -2.0 * position(2);
});
oblateDisplacementField = 0.0;
oblateDisplacementField.ProjectCoefficient(oblateDisplacementCoefficient);
mfem::Vector oblateDisplacement;
oblateDisplacementField.GetTrueDofs(oblateDisplacement);
mfem::Vector oblateDirection(layout.value_offsets().Last());
oblateDirection = 0.0;
stellar_equilibrium_test_utils::assign_value_block(
oblateDirection, layout,
stellar_equilibrium_test_utils::displacementValue, oblateDisplacement);
const mfem::Vector equilibriumDisplacementResidual =
stellar_equilibrium_test_utils::const_residual_view(
equilibriumResidual, layout,
stellar_equilibrium_test_utils::displacementResidual);
const mfem::Vector rotatingDisplacementResidual =
stellar_equilibrium_test_utils::const_residual_view(
rotatingSphericalResidual, layout,
stellar_equilibrium_test_utils::displacementResidual);
/*
* Subtract the nonrotating force-balance projection floor. The remainder
* is the displacement residual introduced by rotation.
*/
mfem::Vector rotationInducedResidual(rotatingDisplacementResidual);
rotationInducedResidual -= equilibriumDisplacementResidual;
const double rotationInducedWork = gravity_prepared_test_utils::global_dot(
rotationInducedResidual, oblateDisplacement, f.mesh->GetComm());
const double rotationInducedNorm =
stellar_equilibrium_test_utils::global_norm(rotationInducedResidual,
f.mesh->GetComm());
const double oblateDirectionNorm =
stellar_equilibrium_test_utils::global_norm(oblateDisplacement,
f.mesh->GetComm());
const double workScale = rotationInducedNorm * oblateDirectionNorm;
INFO("Keplerian angular speed = " << keplerianAngularSpeed);
INFO("Applied angular speed = " << angularSpeed);
INFO("Rotation fraction = " << rotationFraction);
INFO("Rotation-induced displacement residual norm = "
<< rotationInducedNorm);
INFO("Rotation-induced work against the oblate direction = "
<< rotationInducedWork);
INFO("Normalized oblate work = " << rotationInducedWork / workScale);
REQUIRE(std::isfinite(rotationInducedWork));
REQUIRE(std::isfinite(rotationInducedNorm));
REQUIRE(std::isfinite(oblateDirectionNorm));
REQUIRE(rotationInducedNorm > 0.0);
REQUIRE(oblateDirectionNorm > 0.0);
REQUIRE(workScale > 0.0);
/*
* The force residual uses the convention R_rot(w) = -integral rho a_c.w.
* Therefore negative work against this direction means that -R, the
* Newton right-hand side, drives a positive oblate deformation.
*/
CHECK(rotationInducedWork < 0.0);
CHECK(rotationInducedWork < -1.0e-3 * workScale);
/*
* Evaluate the displacement column of the complete coupled Jacobian at
* the rotating spherical state.
*/
mfem::Vector oblateJacobianAction;
stellarOperator.Mult(oblateDirection, oblateJacobianAction);
const mfem::Vector oblateDisplacementJacobianAction =
stellar_equilibrium_test_utils::const_residual_view(
oblateJacobianAction, layout,
stellar_equilibrium_test_utils::displacementResidual);
const double residualDirectionalDerivative =
gravity_prepared_test_utils::global_dot(
rotatingDisplacementResidual, oblateDisplacementJacobianAction,
f.mesh->GetComm());
const double jacobianDirectionNormSquared =
gravity_prepared_test_utils::global_dot(
oblateDisplacementJacobianAction, oblateDisplacementJacobianAction,
f.mesh->GetComm());
REQUIRE(std::isfinite(residualDirectionalDerivative));
REQUIRE(std::isfinite(jacobianDirectionNormSquared));
REQUIRE(jacobianDirectionNormSquared > 0.0);
/*
* Minimize the linearized displacement-residual norm along the oblate
* direction:
*
* alpha_* = -(R_d, J_d p) / ||J_d p||^2.
*
* A positive alpha_* means that the operator selects equatorial expansion
* and polar contraction rather than the prolate direction.
*/
const double optimalLinearizedAmplitude =
-residualDirectionalDerivative / jacobianDirectionNormSquared;
INFO("Displacement-residual directional derivative = "
<< residualDirectionalDerivative);
INFO(
"Optimal linearized oblate amplitude = " << optimalLinearizedAmplitude);
REQUIRE(std::isfinite(optimalLinearizedAmplitude));
CHECK(residualDirectionalDerivative < 0.0);
REQUIRE(optimalLinearizedAmplitude > 0.0);
/*
* Take only a fraction of the predicted step and cap it at a two-percent
* surface deformation. This keeps the test safely inside the local
* linearization regime.
*/
const double appliedOblateAmplitude =
std::min(0.25 * optimalLinearizedAmplitude, 2.0e-2);
REQUIRE(appliedOblateAmplitude > 0.0);
mfem::Vector predictedDisplacementResidual(rotatingDisplacementResidual);
predictedDisplacementResidual.Add(appliedOblateAmplitude,
oblateDisplacementJacobianAction);
const double rotatingDisplacementNorm =
stellar_equilibrium_test_utils::global_norm(
rotatingDisplacementResidual, f.mesh->GetComm());
const double predictedDisplacementNorm =
stellar_equilibrium_test_utils::global_norm(
predictedDisplacementResidual, f.mesh->GetComm());
INFO("Rotating spherical displacement residual norm = "
<< rotatingDisplacementNorm);
INFO("Predicted oblate displacement residual norm = "
<< predictedDisplacementNorm);
CHECK(predictedDisplacementNorm < rotatingDisplacementNorm);
/*
* Apply the same positive oblate displacement to the nonlinear operator.
* Only the displacement row is compared: a complete rotating equilibrium
* also requires simultaneous changes in rho, g, Phi, h, and C.
*/
mfem::Vector oblateState(equilibriumState);
{
mfem::Vector displacementBlock =
stellar_equilibrium_test_utils::value_view(
oblateState, layout,
stellar_equilibrium_test_utils::displacementValue);
displacementBlock.Add(appliedOblateAmplitude, oblateDisplacement);
}
++dependencies.displacement.revision;
stellarOperator.Prepare(oblateState, dependencies, rotation);
mfem::Vector nonlinearOblateResidual;
stellarOperator.BuildResidual(nonlinearOblateResidual);
const double nonlinearOblateDisplacementNorm =
stellar_equilibrium_test_utils::global_norm(
stellar_equilibrium_test_utils::const_residual_view(
nonlinearOblateResidual, layout,
stellar_equilibrium_test_utils::displacementResidual),
f.mesh->GetComm());
const double equatorialRadiusScale = 1.0 + appliedOblateAmplitude;
const double polarRadiusScale = 1.0 - 2.0 * appliedOblateAmplitude;
const double equatorialToPolarRadiusRatio =
equatorialRadiusScale / polarRadiusScale;
INFO("Applied oblate amplitude = " << appliedOblateAmplitude);
INFO("Nonlinear oblate displacement residual norm = "
<< nonlinearOblateDisplacementNorm);
INFO("Equatorial radius scale = " << equatorialRadiusScale);
INFO("Polar radius scale = " << polarRadiusScale);
INFO("Equatorial-to-polar radius ratio = " << equatorialToPolarRadiusRatio);
CHECK(equatorialRadiusScale > 1.0);
CHECK(polarRadiusScale < 1.0);
CHECK(polarRadiusScale > 0.0);
CHECK(equatorialToPolarRadiusRatio > 1.0);
CHECK(nonlinearOblateDisplacementNorm < rotatingDisplacementNorm);
}
}