Files
MeanField/tests/operators/prepared_stellar_equilibrium.cpp
Emily Boudreaux 0f3ca8050b feat(field-support): added field support system, mid migration
currently the barotope and the pressure force operator are migrated to the new support system
2026-08-23 10:13:53 -04:00

2366 lines
97 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_full_gravity_state(
const mean_field::fem::FEM &f,
const mfem::Vector &fullDensity,
const mfem::Vector &displacement,
const mfem::Vector &gravityGradient,
const mfem::Vector &gravityPotential
) {
const std::array<int, 4> blockSizes{
f.densityFes->GetTrueVSize(), f.displacementFes->GetTrueVSize(), f.gravityFluxFes->GetTrueVSize(),
f.gravityPotentialFes->GetTrueVSize()
};
MFEM_VERIFY(fullDensity.Size() == blockSizes[0], "Full density has the wrong gravity-state size.");
MFEM_VERIFY(displacement.Size() == blockSizes[1], "Displacement has the wrong gravity-state size.");
MFEM_VERIFY(gravityGradient.Size() == blockSizes[2], "Gravity gradient has the wrong gravity-state size.");
MFEM_VERIFY(gravityPotential.Size() == blockSizes[3], "Gravity potential has the wrong gravity-state 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{
&fullDensity, &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 FieldMaps maps(f);
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 fullDensity = maps.density.scatter(reducedDensity);
const mfem::Vector gravityState =
pack_full_gravity_state(f, fullDensity, displacement, gravityGradient, gravityPotential);
mfem::Vector gravity;
mfem::Vector closure;
mfem::Vector displacementResidualValue;
mfem::Vector fullHydrostatic;
mfem::Vector mass;
stellarOperator.GetGravityOperator().Mult(gravityState, gravity);
stellarOperator.GetBarotropicClosureOperator().BuildResidual(closure);
stellarOperator.GetDisplacementOperator().BuildResidual(displacementResidualValue);
stellarOperator.GetHydrostaticOperator().BuildResidual(fullHydrostatic);
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;
{
mfem::Vector reducedHydrostatic = residual_view(result, layout, enthalpyResidual);
maps.enthalpy.gather(fullHydrostatic, reducedHydrostatic);
}
residual_view(result, layout, massResidual) = mass;
return result;
}
[[nodiscard]] mfem::Vector explicit_jacobian_action(
const mean_field::operators::PreparedStellarEquilibriumOperator &stellarOperator,
const mean_field::fem::FEM &f,
const mfem::Vector &direction
) {
const mean_field::operators::StellarEquilibriumLayout &layout = stellarOperator.GetLayout();
const FieldMaps maps(f);
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 fullDensityDirection = maps.density.scatter(reducedDensityDirection);
const mfem::Vector fullEnthalpyDirection = maps.enthalpy.scatter(reducedEnthalpyDirection);
const mfem::Vector gravityDirection = pack_full_gravity_state(
f, fullDensityDirection, displacementDirection, gravityGradientDirection, gravityPotentialDirection
);
mfem::Vector gravityAction;
mfem::Vector closureAction;
mfem::Vector displacementAction;
mfem::Vector fullHydrostaticAction;
mfem::Vector massAction;
stellarOperator.GetGravityJacobianOperator().Mult(gravityDirection, gravityAction);
stellarOperator.GetBarotropicClosureOperator().Mult(
reducedDensityDirection, reducedEnthalpyDirection, displacementDirection, closureAction
);
stellarOperator.GetDisplacementOperator().ApplyCompleteJacobianAction(
fullDensityDirection, displacementDirection, gravityGradientDirection, fullEnthalpyDirection,
displacementAction
);
stellarOperator.GetHydrostaticOperator().ApplyCompleteJacobianAction(
fullEnthalpyDirection, gravityPotentialDirection, bernoulliDirection(0), displacementDirection,
fullHydrostaticAction
);
stellarOperator.GetMassNormalizationOperator().ApplyCompleteJacobianAction(
fullDensityDirection, 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;
{
mfem::Vector reducedHydrostaticAction = residual_view(result, layout, enthalpyResidual);
maps.enthalpy.gather(fullHydrostaticAction, reducedHydrostaticAction);
}
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 &tags::prepared &tags::field &tags::jacobian &tags::integration &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(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, f, 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.mapping != nullptr);
REQUIRE(f.domainMapperStateless != nullptr);
f.mapping->ResetDisplacement();
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.mapping, potentialFunction);
LaneEmdenGravityGradientCoefficient gravityGradientCoefficient(
f.mesh->Dimension(), f.domainMapperStateless->GetVacuumElementAttribute(), 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.mapping != 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.mapping->SetDisplacement(displacementField);
mean_field::physics::update_stiffness_matrix(f);
const mean_field::physics::GravitySolution gravitySolution =
mean_field::physics::grav_potential_new(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);
}
}