2440 lines
93 KiB
C++
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);
|
|
}
|
|
}
|