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