restricted the unknown state vector to surface deformation and implemented one prescription, NodalRadialSurface, while the full volumetric displacment field is reconstructed analytically from that. This reduced the number of degrees of freedom in the system by a factor of 80 while also removing many null vectors from the system.
2827 lines
121 KiB
C++
2827 lines
121 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)
|
|
) {
|
|
}
|
|
};
|
|
|
|
[[nodiscard]] auto make_stellar_model(
|
|
const mean_field::eos::Polytrope &equationOfState,
|
|
const double targetMass,
|
|
const mean_field::eos::PressureValue surfacePressure = mean_field::eos::PressureValue{0.0}
|
|
) {
|
|
return mean_field::models::StellarModel{
|
|
mean_field::models::structure::PolytropicStructure{equationOfState, targetMass},
|
|
mean_field::surface::ConstantPressureSurface{surfacePressure}
|
|
};
|
|
}
|
|
|
|
constexpr auto densityValue =
|
|
mean_field::utils::blocks::get_value_block<Form>(mean_field::utils::blocks::density_field.mass_term);
|
|
constexpr auto displacementValue =
|
|
mean_field::utils::blocks::get_value_block<Form>(mean_field::utils::blocks::displacement_field.geometry_term);
|
|
constexpr auto gravityGradientValue =
|
|
mean_field::utils::blocks::get_value_block<Form>(mean_field::utils::blocks::gravity_field.gradient_term);
|
|
constexpr auto gravityPotentialValue =
|
|
mean_field::utils::blocks::get_value_block<Form>(mean_field::utils::blocks::gravity_field.poisson_term);
|
|
constexpr auto enthalpyValue =
|
|
mean_field::utils::blocks::get_value_block<Form>(mean_field::utils::blocks::enthalpy_field.specific_term);
|
|
constexpr auto bernoulliValue = mean_field::utils::blocks::get_value_block<Form>(
|
|
mean_field::utils::blocks::barotropic_constant_field.mass_normalization_term
|
|
);
|
|
|
|
constexpr auto gravityGradientResidual =
|
|
mean_field::utils::blocks::get_residual_block<Form>(mean_field::utils::blocks::gravity_field.gradient_term);
|
|
constexpr auto gravityPotentialResidual =
|
|
mean_field::utils::blocks::get_residual_block<Form>(mean_field::utils::blocks::gravity_field.poisson_term);
|
|
constexpr auto densityResidual =
|
|
mean_field::utils::blocks::get_residual_block<Form>(mean_field::utils::blocks::density_field.mass_term);
|
|
constexpr auto displacementResidual = mean_field::utils::blocks::get_residual_block<Form>(
|
|
mean_field::utils::blocks::displacement_field.geometry_term
|
|
);
|
|
constexpr auto enthalpyResidual =
|
|
mean_field::utils::blocks::get_residual_block<Form>(mean_field::utils::blocks::enthalpy_field.specific_term);
|
|
constexpr auto massResidual = mean_field::utils::blocks::get_residual_block<Form>(
|
|
mean_field::utils::blocks::barotropic_constant_field.mass_normalization_term
|
|
);
|
|
|
|
template <int index>
|
|
[[nodiscard]] mfem::Vector value_view(
|
|
mfem::Vector &vector,
|
|
const mean_field::operators::StellarEquilibriumLayout &layout,
|
|
const mean_field::utils::blocks::value_block<index> block
|
|
) {
|
|
return mfem::Vector(vector.GetData() + layout.offset(block), layout.size(block));
|
|
}
|
|
|
|
template <int index>
|
|
void assign_value_block(
|
|
mfem::Vector &vector,
|
|
const mean_field::operators::StellarEquilibriumLayout &layout,
|
|
const mean_field::utils::blocks::value_block<index> block,
|
|
const mfem::Vector &source
|
|
) {
|
|
MFEM_VERIFY(
|
|
source.Size() == layout.size(block), "Source vector has the wrong size for the coupled value block."
|
|
);
|
|
|
|
const int offset = layout.offset(block);
|
|
|
|
for (int dof = 0; dof < source.Size(); ++dof) {
|
|
vector(offset + dof) = source(dof);
|
|
}
|
|
}
|
|
|
|
template <int index>
|
|
[[nodiscard]] mfem::Vector const_value_view(
|
|
const mfem::Vector &vector,
|
|
const mean_field::operators::StellarEquilibriumLayout &layout,
|
|
const mean_field::utils::blocks::value_block<index> block
|
|
) {
|
|
return mfem::Vector(const_cast<mfem::real_t *>(vector.GetData()) + layout.offset(block), layout.size(block));
|
|
}
|
|
|
|
template <int index>
|
|
[[nodiscard]] mfem::Vector residual_view(
|
|
mfem::Vector &vector,
|
|
const mean_field::operators::StellarEquilibriumLayout &layout,
|
|
const mean_field::utils::blocks::residual_block<index> block
|
|
) {
|
|
return mfem::Vector(vector.GetData() + layout.offset(block), layout.size(block));
|
|
}
|
|
|
|
template <int index>
|
|
[[nodiscard]] mfem::Vector const_residual_view(
|
|
const mfem::Vector &vector,
|
|
const mean_field::operators::StellarEquilibriumLayout &layout,
|
|
const mean_field::utils::blocks::residual_block<index> block
|
|
) {
|
|
return mfem::Vector(const_cast<mfem::real_t *>(vector.GetData()) + layout.offset(block), layout.size(block));
|
|
}
|
|
|
|
[[nodiscard]] mfem::Vector reduce_density(
|
|
const mean_field::fem::FEM &f,
|
|
const mfem::Vector &fullDensity
|
|
) {
|
|
const field::FieldDofMap map = field::make_field_dof_map<field::Density, DomainSchema>(*f.densityFes);
|
|
return map.gather(fullDensity);
|
|
}
|
|
|
|
[[nodiscard]] mfem::Vector reduce_enthalpy(
|
|
const mean_field::fem::FEM &f,
|
|
const mfem::Vector &fullEnthalpy
|
|
) {
|
|
const field::FieldDofMap map = field::make_field_dof_map<field::Enthalpy, DomainSchema>(*f.enthalpyFes);
|
|
return map.gather(fullEnthalpy);
|
|
}
|
|
|
|
[[nodiscard]] mfem::Vector pack_gravity_state(
|
|
const mfem::Vector &density,
|
|
const mfem::Vector &displacement,
|
|
const mfem::Vector &gravityGradient,
|
|
const mfem::Vector &gravityPotential
|
|
) {
|
|
const std::array<int, 4> blockSizes{
|
|
density.Size(), displacement.Size(), gravityGradient.Size(), gravityPotential.Size()
|
|
};
|
|
|
|
const std::array<int, 5> offsets{
|
|
0, blockSizes[0], blockSizes[0] + blockSizes[1], blockSizes[0] + blockSizes[1] + blockSizes[2],
|
|
blockSizes[0] + blockSizes[1] + blockSizes[2] + blockSizes[3]
|
|
};
|
|
|
|
mfem::Vector packed(offsets[4]);
|
|
|
|
const std::array<const mfem::Vector *, 4> blocks{&density, &displacement, &gravityGradient, &gravityPotential};
|
|
|
|
for (int block = 0; block < 4; ++block) {
|
|
mfem::Vector destination(packed.GetData() + offsets[block], blockSizes[block]);
|
|
destination = *blocks[block];
|
|
}
|
|
|
|
return packed;
|
|
}
|
|
|
|
[[nodiscard]] mfem::Vector project_density(
|
|
const mean_field::fem::FEM &f,
|
|
const double phase
|
|
) {
|
|
mfem::ParGridFunction field(f.densityFes.get());
|
|
mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) {
|
|
return 0.88 + 0.07 * std::sin(0.73 * position(0) + phase) + 0.05 * std::cos(0.59 * position(1) - phase) +
|
|
0.025 * position(2) * position(2);
|
|
});
|
|
field.ProjectCoefficient(coefficient);
|
|
mfem::Vector result;
|
|
field.GetTrueDofs(result);
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] mfem::Vector project_density_direction(
|
|
const mean_field::fem::FEM &f,
|
|
const double phase
|
|
) {
|
|
mfem::ParGridFunction field(f.densityFes.get());
|
|
mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) {
|
|
return 0.16 * std::sin(0.91 * position(0) + phase) - 0.12 * std::cos(0.77 * position(1) - phase) +
|
|
0.06 * position(2);
|
|
});
|
|
field.ProjectCoefficient(coefficient);
|
|
mfem::Vector result;
|
|
field.GetTrueDofs(result);
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] mfem::Vector project_constant_density(
|
|
const mean_field::fem::FEM &f,
|
|
const double value
|
|
) {
|
|
mfem::ParGridFunction field(f.densityFes.get());
|
|
mfem::ConstantCoefficient coefficient(value);
|
|
field.ProjectCoefficient(coefficient);
|
|
mfem::Vector result;
|
|
field.GetTrueDofs(result);
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] mfem::Vector project_displacement(
|
|
const mean_field::fem::FEM &f,
|
|
const double scale
|
|
) {
|
|
return gravity_prepared_test_utils::make_displacement(f, scale);
|
|
}
|
|
|
|
[[nodiscard]] mfem::Vector project_displacement_direction(
|
|
const mean_field::fem::FEM &f,
|
|
const double scale
|
|
) {
|
|
mfem::ParGridFunction field(f.displacementFes.get());
|
|
mfem::VectorFunctionCoefficient coefficient(
|
|
f.mesh->Dimension(), [scale](const mfem::Vector &position, mfem::Vector &value) {
|
|
value.SetSize(3);
|
|
value(0) = scale * (0.06 * position(0) + 0.014 * position(1) * position(2));
|
|
value(1) = scale * (-0.045 * position(1) + 0.011 * position(0) * position(2));
|
|
value(2) = scale * (0.035 * position(2) - 0.009 * position(0) * position(1));
|
|
}
|
|
);
|
|
field.ProjectCoefficient(coefficient);
|
|
mfem::Vector result;
|
|
field.GetTrueDofs(result);
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] mfem::Vector project_gravity_gradient(
|
|
const mean_field::fem::FEM &f,
|
|
const double phase
|
|
) {
|
|
mfem::ParGridFunction field(f.gravityFluxFes.get());
|
|
mfem::VectorFunctionCoefficient coefficient(
|
|
f.mesh->Dimension(), [phase](const mfem::Vector &position, mfem::Vector &value) {
|
|
value.SetSize(3);
|
|
value(0) = 0.27 + 0.07 * position(0) + 0.025 * phase * position(1);
|
|
value(1) = -0.19 + 0.055 * position(1) - 0.018 * phase * position(2);
|
|
value(2) = 0.21 - 0.045 * position(2) + 0.021 * phase * position(0);
|
|
}
|
|
);
|
|
field.ProjectCoefficient(coefficient);
|
|
mfem::Vector result;
|
|
field.GetTrueDofs(result);
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] mfem::Vector project_gravity_direction(
|
|
const mean_field::fem::FEM &f,
|
|
const double phase
|
|
) {
|
|
mfem::ParGridFunction field(f.gravityFluxFes.get());
|
|
mfem::VectorFunctionCoefficient coefficient(
|
|
f.mesh->Dimension(), [phase](const mfem::Vector &position, mfem::Vector &value) {
|
|
value.SetSize(3);
|
|
value(0) = 0.13 * std::sin(position(0) + phase) + 0.025 * position(1);
|
|
value(1) = -0.10 * std::cos(position(1) - phase) + 0.035 * position(2);
|
|
value(2) = 0.08 * std::sin(position(2) + 0.5 * phase) - 0.018 * position(0);
|
|
}
|
|
);
|
|
field.ProjectCoefficient(coefficient);
|
|
mfem::Vector result;
|
|
field.GetTrueDofs(result);
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] mfem::Vector project_gravity_potential(
|
|
const mean_field::fem::FEM &f,
|
|
const double phase
|
|
) {
|
|
mfem::ParGridFunction field(f.gravityPotentialFes.get());
|
|
mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) {
|
|
return 0.24 + 0.09 * std::sin(0.67 * position(0) + phase) - 0.06 * std::cos(0.53 * position(1) - phase) +
|
|
0.035 * position(2);
|
|
});
|
|
field.ProjectCoefficient(coefficient);
|
|
mfem::Vector result;
|
|
field.GetTrueDofs(result);
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] mfem::Vector project_potential_direction(
|
|
const mean_field::fem::FEM &f,
|
|
const double phase
|
|
) {
|
|
mfem::ParGridFunction field(f.gravityPotentialFes.get());
|
|
mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) {
|
|
return 0.17 * std::sin(0.81 * position(0) + phase) + 0.11 * std::cos(0.69 * position(1) - phase) -
|
|
0.07 * position(2);
|
|
});
|
|
field.ProjectCoefficient(coefficient);
|
|
mfem::Vector result;
|
|
field.GetTrueDofs(result);
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] mfem::Vector project_enthalpy(
|
|
const mean_field::fem::FEM &f,
|
|
const double phase
|
|
) {
|
|
mfem::ParGridFunction field(f.enthalpyFes.get());
|
|
mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) {
|
|
return 0.82 + 0.08 * std::sin(0.62 * position(0) + phase) + 0.045 * std::cos(0.57 * position(1) - phase) +
|
|
0.02 * position(2) * position(2);
|
|
});
|
|
field.ProjectCoefficient(coefficient);
|
|
mfem::Vector result;
|
|
field.GetTrueDofs(result);
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] mfem::Vector project_enthalpy_direction(
|
|
const mean_field::fem::FEM &f,
|
|
const double phase
|
|
) {
|
|
mfem::ParGridFunction field(f.enthalpyFes.get());
|
|
mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) {
|
|
return 0.21 * std::sin(0.74 * position(0) + phase) - 0.14 * std::cos(0.64 * position(1) - phase) +
|
|
0.075 * position(2);
|
|
});
|
|
field.ProjectCoefficient(coefficient);
|
|
mfem::Vector result;
|
|
field.GetTrueDofs(result);
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] mfem::Vector project_constant_scalar(
|
|
mfem::ParFiniteElementSpace &finiteElementSpace,
|
|
const double value
|
|
) {
|
|
mfem::ParGridFunction field(&finiteElementSpace);
|
|
mfem::ConstantCoefficient coefficient(value);
|
|
field.ProjectCoefficient(coefficient);
|
|
mfem::Vector result;
|
|
field.GetTrueDofs(result);
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] mean_field::physics::RigidRotation make_rotation(const double scale) {
|
|
mfem::Vector angularVelocity(3);
|
|
angularVelocity(0) = scale * 0.16;
|
|
angularVelocity(1) = scale * -0.08;
|
|
angularVelocity(2) = scale * 0.58;
|
|
|
|
mfem::Vector center(3);
|
|
center(0) = 0.03;
|
|
center(1) = -0.025;
|
|
center(2) = 0.015;
|
|
return mean_field::physics::RigidRotation(angularVelocity, center);
|
|
}
|
|
|
|
[[nodiscard]] mean_field::physics::RigidRotation make_zero_rotation() {
|
|
return make_rotation(0.0);
|
|
}
|
|
|
|
[[nodiscard]] mean_field::operators::StellarEquilibriumDependencies make_dependencies() {
|
|
return {
|
|
.discretization = {.identity = 1009, .revision = 3},
|
|
.density = {.identity = 1013, .revision = 5},
|
|
.surfaceDeformation = {.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.surfaceDeformation.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);
|
|
}
|
|
|
|
mfem::Vector surfaceDeformation(layout.size(displacementValue));
|
|
surfaceDeformation = 0.0;
|
|
assign_value_block(state, layout, displacementValue, surfaceDeformation);
|
|
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);
|
|
}
|
|
|
|
mfem::Vector surfaceDeformationDirection(layout.size(displacementValue));
|
|
for (int parameter = 0; parameter < surfaceDeformationDirection.Size(); ++parameter) {
|
|
surfaceDeformationDirection(parameter) = 0.11 * std::cos(0.41 * static_cast<double>(parameter) + 0.79);
|
|
}
|
|
assign_value_block(direction, layout, displacementValue, surfaceDeformationDirection);
|
|
assign_value_block(direction, layout, gravityGradientValue, project_gravity_direction(f, 0.83));
|
|
assign_value_block(direction, layout, gravityPotentialValue, project_potential_direction(f, 0.89));
|
|
|
|
{
|
|
const mfem::Vector fullEnthalpyDirection = project_enthalpy_direction(f, 0.97);
|
|
const mfem::Vector reducedEnthalpyDirection = maps.enthalpy.gather(fullEnthalpyDirection);
|
|
assign_value_block(direction, layout, enthalpyValue, reducedEnthalpyDirection);
|
|
}
|
|
|
|
value_view(direction, layout, bernoulliValue)(0) = -0.37;
|
|
|
|
return direction;
|
|
}
|
|
|
|
[[nodiscard]] double global_norm(
|
|
const mfem::Vector &vector,
|
|
const MPI_Comm communicator
|
|
) {
|
|
return gravity_prepared_test_utils::global_norm(vector, communicator);
|
|
}
|
|
|
|
[[nodiscard]] double relative_difference(
|
|
const mfem::Vector &left,
|
|
const mfem::Vector &right,
|
|
const MPI_Comm communicator
|
|
) {
|
|
mfem::Vector difference(left);
|
|
difference -= right;
|
|
const double scale = std::max(
|
|
{global_norm(left, communicator), global_norm(right, communicator),
|
|
100.0 * std::numeric_limits<double>::epsilon()}
|
|
);
|
|
return global_norm(difference, communicator) / scale;
|
|
}
|
|
|
|
[[nodiscard]] mfem::Vector explicit_residual(
|
|
const mean_field::operators::PreparedStellarEquilibriumOperator &stellarOperator,
|
|
const mean_field::fem::FEM &f,
|
|
const mfem::Vector &state
|
|
) {
|
|
const mean_field::operators::StellarEquilibriumLayout &layout = stellarOperator.GetLayout();
|
|
const mfem::Vector reducedDensity = const_value_view(state, layout, densityValue);
|
|
const mfem::Vector &displacement = stellarOperator.GetGeneratedVolumeDisplacement();
|
|
const mfem::Vector gravityGradient = const_value_view(state, layout, gravityGradientValue);
|
|
const mfem::Vector gravityPotential = const_value_view(state, layout, gravityPotentialValue);
|
|
|
|
const mfem::Vector gravityState =
|
|
pack_gravity_state(reducedDensity, displacement, gravityGradient, gravityPotential);
|
|
|
|
mfem::Vector gravity;
|
|
mfem::Vector closure;
|
|
mfem::Vector displacementResidualValue;
|
|
mfem::Vector hydrostatic;
|
|
mfem::Vector mass;
|
|
|
|
stellarOperator.GetGravityOperator().Mult(gravityState, gravity);
|
|
stellarOperator.GetBarotropicClosureOperator().BuildResidual(closure);
|
|
stellarOperator.GetDisplacementOperator().BuildResidual(displacementResidualValue);
|
|
mfem::Vector surfaceShapeResidualValue(stellarOperator.GetDomainDeformation().parameterCount());
|
|
stellarOperator.GetDomainDeformation().applyJacobianTranspose(
|
|
stellarOperator.GetSurfaceDeformationParameters(), displacementResidualValue, surfaceShapeResidualValue
|
|
);
|
|
stellarOperator.GetHydrostaticOperator().BuildResidual(hydrostatic);
|
|
stellarOperator.GetSurfaceConstraintOperator().ApplyResidualRows(hydrostatic);
|
|
stellarOperator.GetMassNormalizationOperator().BuildResidual(mass);
|
|
|
|
mfem::Vector result(layout.residual_offsets().Last());
|
|
result = 0.0;
|
|
|
|
MFEM_VERIFY(
|
|
gravity.Size() == layout.size(gravityGradientResidual) + layout.size(gravityPotentialResidual),
|
|
"Explicit gravity residual has the wrong size."
|
|
);
|
|
|
|
const mfem::Vector gravityGradientResidualValue(gravity.GetData(), layout.size(gravityGradientResidual));
|
|
const mfem::Vector gravityPotentialResidualValue(
|
|
gravity.GetData() + layout.size(gravityGradientResidual), layout.size(gravityPotentialResidual)
|
|
);
|
|
|
|
residual_view(result, layout, gravityGradientResidual) = gravityGradientResidualValue;
|
|
residual_view(result, layout, gravityPotentialResidual) = gravityPotentialResidualValue;
|
|
|
|
residual_view(result, layout, densityResidual) = closure;
|
|
|
|
residual_view(result, layout, displacementResidual) = surfaceShapeResidualValue;
|
|
|
|
residual_view(result, layout, enthalpyResidual) = hydrostatic;
|
|
|
|
residual_view(result, layout, massResidual) = mass;
|
|
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] mfem::Vector explicit_jacobian_action(
|
|
const mean_field::operators::PreparedStellarEquilibriumOperator &stellarOperator,
|
|
const mfem::Vector &direction
|
|
) {
|
|
const mean_field::operators::StellarEquilibriumLayout &layout = stellarOperator.GetLayout();
|
|
|
|
const mfem::Vector reducedDensityDirection = const_value_view(direction, layout, densityValue);
|
|
const mfem::Vector surfaceDeformationDirection = const_value_view(direction, layout, displacementValue);
|
|
mfem::Vector displacementDirection(stellarOperator.GetDomainDeformation().volumeDisplacementSize());
|
|
stellarOperator.GetDomainDeformation().applyJacobian(
|
|
stellarOperator.GetSurfaceDeformationParameters(), surfaceDeformationDirection, displacementDirection
|
|
);
|
|
const mfem::Vector gravityGradientDirection = const_value_view(direction, layout, gravityGradientValue);
|
|
const mfem::Vector gravityPotentialDirection = const_value_view(direction, layout, gravityPotentialValue);
|
|
const mfem::Vector reducedEnthalpyDirection = const_value_view(direction, layout, enthalpyValue);
|
|
const mfem::Vector bernoulliDirection = const_value_view(direction, layout, bernoulliValue);
|
|
|
|
const mfem::Vector gravityDirection = pack_gravity_state(
|
|
reducedDensityDirection, displacementDirection, gravityGradientDirection, gravityPotentialDirection
|
|
);
|
|
|
|
mfem::Vector gravityAction;
|
|
mfem::Vector closureAction;
|
|
mfem::Vector displacementAction;
|
|
mfem::Vector hydrostaticAction;
|
|
mfem::Vector massAction;
|
|
|
|
stellarOperator.GetGravityJacobianOperator().Mult(gravityDirection, gravityAction);
|
|
|
|
stellarOperator.GetBarotropicClosureOperator().Mult(
|
|
reducedDensityDirection, reducedEnthalpyDirection, displacementDirection, closureAction
|
|
);
|
|
|
|
stellarOperator.GetDisplacementOperator().ApplyCompleteJacobianAction(
|
|
reducedDensityDirection, displacementDirection, gravityGradientDirection, reducedEnthalpyDirection,
|
|
displacementAction
|
|
);
|
|
mfem::Vector surfaceShapeAction(stellarOperator.GetDomainDeformation().parameterCount());
|
|
stellarOperator.GetDomainDeformation().applyJacobianTranspose(
|
|
stellarOperator.GetSurfaceDeformationParameters(), displacementAction, surfaceShapeAction
|
|
);
|
|
mfem::Vector pullbackDerivative(stellarOperator.GetDomainDeformation().parameterCount());
|
|
stellarOperator.GetDomainDeformation().applyPullbackDerivative(
|
|
stellarOperator.GetSurfaceDeformationParameters(), surfaceDeformationDirection,
|
|
stellarOperator.GetFullMechanicalResidual(), pullbackDerivative
|
|
);
|
|
surfaceShapeAction += pullbackDerivative;
|
|
|
|
stellarOperator.GetHydrostaticOperator().ApplyCompleteJacobianAction(
|
|
reducedEnthalpyDirection, gravityPotentialDirection, bernoulliDirection(0), displacementDirection,
|
|
hydrostaticAction
|
|
);
|
|
stellarOperator.GetSurfaceConstraintOperator().ApplyJacobianRows(reducedEnthalpyDirection, hydrostaticAction);
|
|
|
|
stellarOperator.GetMassNormalizationOperator().ApplyCompleteJacobianAction(
|
|
reducedDensityDirection, displacementDirection, massAction
|
|
);
|
|
|
|
mfem::Vector result(layout.residual_offsets().Last());
|
|
result = 0.0;
|
|
|
|
MFEM_VERIFY(
|
|
gravityAction.Size() == layout.size(gravityGradientResidual) + layout.size(gravityPotentialResidual),
|
|
"Explicit gravity Jacobian action has the wrong size."
|
|
);
|
|
|
|
const mfem::Vector gravityGradientAction(gravityAction.GetData(), layout.size(gravityGradientResidual));
|
|
const mfem::Vector gravityPotentialAction(
|
|
gravityAction.GetData() + layout.size(gravityGradientResidual), layout.size(gravityPotentialResidual)
|
|
);
|
|
|
|
residual_view(result, layout, gravityGradientResidual) = gravityGradientAction;
|
|
residual_view(result, layout, gravityPotentialResidual) = gravityPotentialAction;
|
|
|
|
residual_view(result, layout, densityResidual) = closureAction;
|
|
|
|
residual_view(result, layout, displacementResidual) = surfaceShapeAction;
|
|
|
|
residual_view(result, layout, enthalpyResidual) = hydrostaticAction;
|
|
|
|
residual_view(result, layout, massResidual) = massAction;
|
|
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] long long global_sum(
|
|
const int localValue,
|
|
const MPI_Comm communicator
|
|
) {
|
|
const long long local = static_cast<long long>(localValue);
|
|
long long global = 0;
|
|
|
|
MPI_Allreduce(&local, &global, 1, MPI_LONG_LONG, MPI_SUM, communicator);
|
|
|
|
return global;
|
|
}
|
|
|
|
template <int index>
|
|
[[nodiscard]] double block_relative_difference(
|
|
const mfem::Vector &left,
|
|
const mfem::Vector &right,
|
|
const mean_field::operators::StellarEquilibriumLayout &layout,
|
|
const mean_field::utils::blocks::residual_block<index> block,
|
|
const MPI_Comm communicator
|
|
) {
|
|
return relative_difference(
|
|
const_residual_view(left, layout, block), const_residual_view(right, layout, block), communicator
|
|
);
|
|
}
|
|
} // namespace stellar_equilibrium_test_utils
|
|
|
|
TEST_CASE(
|
|
"Prepared Stellar Equilibrium Uses Surface Parameters For Its Root Geometry Block",
|
|
tags::reduced_stellar_geometry &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::models::StellarModel stellarModel{
|
|
mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{3.0, 0.25}, 1.0},
|
|
mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.0}}
|
|
};
|
|
mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator(
|
|
f, *f.domainMapperStateless, stellarModel
|
|
);
|
|
|
|
CHECK(stellarOperator.GetTargetMass() == stellarModel.targetMass());
|
|
CHECK(stellarOperator.GetDomainDeformation().matchesCurrentDiscretization());
|
|
const mean_field::field::ScalarBoundaryDofMap surfaceDeformationMap =
|
|
mean_field::field::make_stellar_surface_scalar_dof_map<stellar_equilibrium_test_utils::DomainSchema>(
|
|
*f.surfaceDeformationFes
|
|
);
|
|
CHECK(stellarOperator.GetDomainDeformation().parameterCount() == surfaceDeformationMap.local_size());
|
|
CHECK(stellarOperator.GetDomainDeformation().volumeDisplacementSize() == f.displacementFes->GetTrueVSize());
|
|
|
|
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) ==
|
|
stellarOperator.GetDomainDeformation().parameterCount()
|
|
);
|
|
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) ==
|
|
stellarOperator.GetDomainDeformation().parameterCount()
|
|
);
|
|
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 Lifted And Pulled Back Full Child Jacobian",
|
|
tags::barotrope_prepared_jacobian_accuracy &tags::reduced_stellar_geometry &tags::field
|
|
) {
|
|
mean_field::utils::Args args = test_utils::setup_args();
|
|
mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0);
|
|
REQUIRE(f.okay());
|
|
|
|
const mean_field::eos::Polytrope barotrope(3.0, 0.25);
|
|
const auto stellarModel = stellar_equilibrium_test_utils::make_stellar_model(barotrope, 1.19);
|
|
mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator(
|
|
f, *f.domainMapperStateless, stellarModel
|
|
);
|
|
|
|
const auto &layout = stellarOperator.GetLayout();
|
|
const mfem::Vector state = stellar_equilibrium_test_utils::make_state(f, layout);
|
|
const mfem::Vector direction = stellar_equilibrium_test_utils::make_direction(f, layout);
|
|
const auto dependencies = stellar_equilibrium_test_utils::make_dependencies();
|
|
const auto rotation = stellar_equilibrium_test_utils::make_rotation(0.82);
|
|
|
|
stellarOperator.Prepare(state, dependencies, rotation);
|
|
|
|
mfem::Vector rootAction;
|
|
stellarOperator.Mult(direction, rootAction);
|
|
|
|
const mfem::Vector explicitAction =
|
|
stellar_equilibrium_test_utils::explicit_jacobian_action(stellarOperator, direction);
|
|
|
|
const double difference =
|
|
stellar_equilibrium_test_utils::relative_difference(rootAction, explicitAction, f.mesh->GetComm());
|
|
|
|
INFO("Reduced root versus explicit R J P relative difference = " << difference);
|
|
|
|
CHECK(difference < 2.0e-15);
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Prepared Stellar Equilibrium Replaces Stellar Surface Rows With The Compiled Physical Constraint",
|
|
tags::surface_row_replacement
|
|
) {
|
|
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 equationOfState(3.0, 0.25);
|
|
constexpr double targetPressure = 0.03125;
|
|
const auto stellarModel = stellar_equilibrium_test_utils::make_stellar_model(
|
|
equationOfState, 1.19, mean_field::eos::PressureValue{targetPressure}
|
|
);
|
|
|
|
STATIC_CHECK(
|
|
mean_field::operators::SingleFieldPressureSurfaceConstraintFor<
|
|
typename std::remove_cvref_t<decltype(stellarModel)>::SurfaceConstraintType, mean_field::field::Enthalpy>
|
|
);
|
|
|
|
mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator(
|
|
f, *f.domainMapperStateless, stellarModel
|
|
);
|
|
|
|
const auto &layout = stellarOperator.GetLayout();
|
|
const mfem::Vector state = stellar_equilibrium_test_utils::make_state(f, layout);
|
|
const auto report = stellarOperator.Prepare(
|
|
state, stellar_equilibrium_test_utils::make_dependencies(), stellar_equilibrium_test_utils::make_rotation(0.82)
|
|
);
|
|
|
|
CHECK(report.surfaceConstraint.cachedSurfaceState);
|
|
|
|
const auto &surfaceRows = stellarOperator.GetSurfaceConstraintOperator().GetSurfaceRows();
|
|
CHECK(stellar_equilibrium_test_utils::global_sum(surfaceRows.size(), f.mesh->GetComm()) > 0);
|
|
|
|
const double requiredSurfaceEnthalpy = mean_field::eos::evaluate<mean_field::eos::quantity::SpecificEnthalpy>(
|
|
equationOfState, mean_field::eos::PressureValue{targetPressure}
|
|
)
|
|
.value();
|
|
|
|
mfem::Vector residual;
|
|
stellarOperator.BuildResidual(residual);
|
|
const mfem::Vector enthalpyState =
|
|
stellar_equilibrium_test_utils::const_value_view(state, layout, stellar_equilibrium_test_utils::enthalpyValue);
|
|
const mfem::Vector enthalpyResidual = stellar_equilibrium_test_utils::const_residual_view(
|
|
residual, layout, stellar_equilibrium_test_utils::enthalpyResidual
|
|
);
|
|
|
|
for (const int reducedDof : surfaceRows.reduced_dofs()) {
|
|
CAPTURE(reducedDof);
|
|
CHECK(std::abs(enthalpyResidual(reducedDof) - (enthalpyState(reducedDof) - requiredSurfaceEnthalpy)) < 1.0e-13);
|
|
}
|
|
|
|
const mfem::Vector direction = stellar_equilibrium_test_utils::make_direction(f, layout);
|
|
mfem::Vector action;
|
|
stellarOperator.Mult(direction, action);
|
|
const mfem::Vector enthalpyDirection = stellar_equilibrium_test_utils::const_value_view(
|
|
direction, layout, stellar_equilibrium_test_utils::enthalpyValue
|
|
);
|
|
const mfem::Vector enthalpyAction = stellar_equilibrium_test_utils::const_residual_view(
|
|
action, layout, stellar_equilibrium_test_utils::enthalpyResidual
|
|
);
|
|
|
|
for (const int reducedDof : surfaceRows.reduced_dofs()) {
|
|
CAPTURE(reducedDof);
|
|
CHECK(std::abs(enthalpyAction(reducedDof) - enthalpyDirection(reducedDof)) < 1.0e-13);
|
|
}
|
|
|
|
mfem::Vector nonEnthalpyDirection(direction);
|
|
stellar_equilibrium_test_utils::value_view(
|
|
nonEnthalpyDirection, layout, stellar_equilibrium_test_utils::enthalpyValue
|
|
) = 0.0;
|
|
stellarOperator.Mult(nonEnthalpyDirection, action);
|
|
|
|
const mfem::Vector nonEnthalpySurfaceAction = stellar_equilibrium_test_utils::const_residual_view(
|
|
action, layout, stellar_equilibrium_test_utils::enthalpyResidual
|
|
);
|
|
for (const int reducedDof : surfaceRows.reduced_dofs()) {
|
|
CAPTURE(reducedDof);
|
|
CHECK(nonEnthalpySurfaceAction(reducedDof) == 0.0);
|
|
}
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Reduced Stellar Geometry Uses Surface Parameters And Generates An Orientation Preserving Volume Map",
|
|
tags::reduced_stellar_geometry &tags::prepared
|
|
) {
|
|
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 equationOfState(3.0, 0.25);
|
|
const auto stellarModel = stellar_equilibrium_test_utils::make_stellar_model(equationOfState, 1.19);
|
|
mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator(
|
|
f, *f.domainMapperStateless, stellarModel
|
|
);
|
|
|
|
const auto &layout = stellarOperator.GetLayout();
|
|
const mfem::Vector state = stellar_equilibrium_test_utils::make_state(f, layout);
|
|
|
|
const auto report = stellarOperator.Prepare(
|
|
state, stellar_equilibrium_test_utils::make_dependencies(), stellar_equilibrium_test_utils::make_zero_rotation()
|
|
);
|
|
|
|
const int surfaceParameterCount = stellarOperator.GetDomainDeformation().parameterCount();
|
|
const int volumeDisplacementSize = stellarOperator.GetDomainDeformation().volumeDisplacementSize();
|
|
|
|
CHECK(layout.size(stellar_equilibrium_test_utils::displacementValue) == surfaceParameterCount);
|
|
CHECK(layout.size(stellar_equilibrium_test_utils::displacementResidual) == surfaceParameterCount);
|
|
CHECK(surfaceParameterCount < volumeDisplacementSize);
|
|
CHECK(stellarOperator.GetSurfaceDeformationParameters().Size() == surfaceParameterCount);
|
|
CHECK(stellarOperator.GetGeneratedVolumeDisplacement().Size() == volumeDisplacementSize);
|
|
CHECK(report.generatedVolumeDisplacement);
|
|
CHECK(report.generatedGeometry.isOrientationPreserving());
|
|
CHECK(report.generatedDisplacement.identity != 0);
|
|
CHECK(report.generatedDisplacement.revision == 1);
|
|
}
|
|
|
|
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);
|
|
const auto stellarModel = stellar_equilibrium_test_utils::make_stellar_model(barotrope, 1.13);
|
|
Operator stellarOperator(f, *f.domainMapperStateless, stellarModel);
|
|
|
|
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.surfaceConstraint.DidAnyWork());
|
|
CHECK(report.generatedVolumeDisplacement);
|
|
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 = mean_field::eos::evaluate<mean_field::eos::quantity::Density>(
|
|
barotrope, mean_field::eos::SpecificEnthalpyValue{enthalpy}
|
|
)
|
|
.value();
|
|
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);
|
|
|
|
const auto surfacePressure = mean_field::eos::evaluate<mean_field::eos::quantity::Pressure>(
|
|
barotrope, mean_field::eos::SpecificEnthalpyValue{enthalpy}
|
|
);
|
|
const auto stellarModel =
|
|
stellar_equilibrium_test_utils::make_stellar_model(barotrope, targetMass, surfacePressure);
|
|
|
|
mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator(
|
|
f, *f.domainMapperStateless, stellarModel
|
|
);
|
|
|
|
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);
|
|
const auto stellarModel = stellar_equilibrium_test_utils::make_stellar_model(barotrope, 1.0);
|
|
mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator(
|
|
f, *f.domainMapperStateless, stellarModel
|
|
);
|
|
|
|
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);
|
|
const auto stellarModel = stellar_equilibrium_test_utils::make_stellar_model(barotrope, 1.17);
|
|
mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator(
|
|
f, *f.domainMapperStateless, stellarModel
|
|
);
|
|
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::reduced_stellar_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);
|
|
const auto stellarModel = stellar_equilibrium_test_utils::make_stellar_model(barotrope, 1.21);
|
|
mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator(
|
|
f, *f.domainMapperStateless, stellarModel
|
|
);
|
|
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);
|
|
const auto stellarModel = stellar_equilibrium_test_utils::make_stellar_model(barotrope, 1.09);
|
|
mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator(
|
|
f, *f.domainMapperStateless, stellarModel
|
|
);
|
|
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 &tags::reduced_stellar_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);
|
|
const auto stellarModel = stellar_equilibrium_test_utils::make_stellar_model(barotrope, 1.15);
|
|
mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator(
|
|
f, *f.domainMapperStateless, stellarModel
|
|
);
|
|
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 std::uint64_t geometryBuilds = stellarOperator.GetStatistics().generatedGeometryBuilds;
|
|
const auto generatedDisplacement = stellarOperator.GetGeneratedDisplacementDependency();
|
|
|
|
const auto repeated = stellarOperator.Prepare(state, dependencies, rotation);
|
|
CHECK_FALSE(repeated.DidAnyWork());
|
|
CHECK(stellarOperator.GetStatistics().residualAssemblies == rootAssemblies);
|
|
CHECK(stellarOperator.GetStatistics().generatedGeometryBuilds == geometryBuilds);
|
|
CHECK(stellarOperator.GetGeneratedDisplacementDependency() == generatedDisplacement);
|
|
|
|
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);
|
|
CHECK(stellarOperator.GetStatistics().generatedGeometryBuilds == geometryBuilds);
|
|
|
|
mfem::Vector surfaceDeformation =
|
|
stellar_equilibrium_test_utils::value_view(state, layout, stellar_equilibrium_test_utils::displacementValue);
|
|
for (int parameter = 0; parameter < surfaceDeformation.Size(); ++parameter) {
|
|
surfaceDeformation(parameter) += 1.0e-4;
|
|
}
|
|
++dependencies.surfaceDeformation.revision;
|
|
|
|
const auto surfaceDeformationReport = stellarOperator.Prepare(state, dependencies, rotation);
|
|
CHECK(surfaceDeformationReport.generatedVolumeDisplacement);
|
|
CHECK(surfaceDeformationReport.generatedGeometry.isOrientationPreserving());
|
|
CHECK(surfaceDeformationReport.generatedDisplacement.identity == generatedDisplacement.identity);
|
|
CHECK(surfaceDeformationReport.generatedDisplacement.revision == generatedDisplacement.revision + 1);
|
|
CHECK(stellarOperator.GetStatistics().generatedGeometryBuilds == geometryBuilds + 1);
|
|
CHECK(surfaceDeformationReport.gravity.DidAnyWork());
|
|
CHECK(surfaceDeformationReport.barotropicClosure.DidAnyWork());
|
|
CHECK(surfaceDeformationReport.hydrostatic.DidAnyWork());
|
|
CHECK(surfaceDeformationReport.displacement.DidAnyWork());
|
|
CHECK(surfaceDeformationReport.massNormalization.DidAnyWork());
|
|
|
|
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(
|
|
"Accepted Reduced Geometries Remain Valid Across Prepared Stellar Physics Quadrature Rules",
|
|
tags::reduced_stellar_geometry &tags::prepared &tags::geometry &tags::self_consistency
|
|
) {
|
|
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);
|
|
const auto stellarModel = stellar_equilibrium_test_utils::make_stellar_model(barotrope, 1.15);
|
|
auto parameterGeometry = stellarModel.compileDomainDeformation(f);
|
|
const auto &surface = parameterGeometry.surfaceDeformationPrescription();
|
|
|
|
mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator(
|
|
f, *f.domainMapperStateless, stellarModel
|
|
);
|
|
const auto &layout = stellarOperator.GetLayout();
|
|
mfem::Vector state = stellar_equilibrium_test_utils::make_state(f, layout);
|
|
auto dependencies = stellar_equilibrium_test_utils::make_dependencies();
|
|
|
|
mfem::Vector uniformExpansion(surface.parameterCount());
|
|
mfem::Vector uniformContraction(surface.parameterCount());
|
|
mfem::Vector oblateSurface(surface.parameterCount());
|
|
uniformExpansion = 1.0e-2;
|
|
uniformContraction = -1.0e-2;
|
|
for (int parameter = 0; parameter < surface.parameterCount(); ++parameter) {
|
|
const double polarDirection = surface.radialDirection(parameter, 2);
|
|
oblateSurface(parameter) =
|
|
1.0e-2 * surface.referenceRadius(parameter) * (1.0 - 3.0 * polarDirection * polarDirection);
|
|
}
|
|
|
|
const std::array<const mfem::Vector *, 3> shapes{
|
|
&uniformExpansion,
|
|
&uniformContraction,
|
|
&oblateSurface,
|
|
};
|
|
|
|
for (int shape = 0; shape < static_cast<int>(shapes.size()); ++shape) {
|
|
stellar_equilibrium_test_utils::assign_value_block(
|
|
state, layout, stellar_equilibrium_test_utils::displacementValue, *shapes[shape]
|
|
);
|
|
if (shape > 0) {
|
|
++dependencies.surfaceDeformation.revision;
|
|
}
|
|
|
|
const auto report =
|
|
stellarOperator.Prepare(state, dependencies, stellar_equilibrium_test_utils::make_zero_rotation());
|
|
|
|
CAPTURE(shape);
|
|
CHECK(report.generatedVolumeDisplacement);
|
|
CHECK(report.generatedGeometry.isOrientationPreserving());
|
|
CHECK(std::isfinite(report.generatedGeometry.minimumJacobianDeterminant));
|
|
|
|
const auto independentGeometry = stellarOperator.GetDomainDeformation().inspectMappedGeometry(
|
|
stellarOperator.GetGeneratedVolumeDisplacement()
|
|
);
|
|
CHECK(independentGeometry.isOrientationPreserving());
|
|
CHECK(
|
|
std::abs(
|
|
independentGeometry.minimumJacobianDeterminant - report.generatedGeometry.minimumJacobianDeterminant
|
|
) <= 64.0 * std::numeric_limits<double>::epsilon() *
|
|
std::max(1.0, std::abs(report.generatedGeometry.minimumJacobianDeterminant))
|
|
);
|
|
|
|
mfem::Vector residual;
|
|
stellarOperator.BuildResidual(residual);
|
|
for (int entry = 0; entry < residual.Size(); ++entry) {
|
|
REQUIRE(std::isfinite(residual(entry)));
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Reduced Stellar Equilibrium Preserves The Analytic N1 Floor Virtual Work And Rotational Shape Descent",
|
|
tags::barotrope &tags::prepared &tags::analytic_comparison &tags::accuracy &tags::gravity &tags::hydro
|
|
&tags::residuals
|
|
) {
|
|
class LaneEmdenGravityGradientCoefficient final : public mfem::VectorCoefficient {
|
|
public:
|
|
LaneEmdenGravityGradientCoefficient(
|
|
const int dimension,
|
|
const int vacuumAttribute,
|
|
const double stellarRadius,
|
|
const double centralDensity,
|
|
const double targetMass,
|
|
const double polytropicConstant
|
|
)
|
|
: mfem::VectorCoefficient(dimension),
|
|
m_vacuumAttribute(vacuumAttribute),
|
|
m_stellarRadius(stellarRadius),
|
|
m_centralDensity(centralDensity),
|
|
m_targetMass(targetMass),
|
|
m_polytropicConstant(polytropicConstant) {
|
|
}
|
|
|
|
void Eval(
|
|
mfem::Vector &value,
|
|
mfem::ElementTransformation &transformation,
|
|
const mfem::IntegrationPoint &integrationPoint
|
|
) override {
|
|
mfem::Vector computationalPosition;
|
|
transformation.Transform(integrationPoint, computationalPosition);
|
|
|
|
value.SetSize(vdim);
|
|
value = 0.0;
|
|
|
|
const double radius = computationalPosition.Norml2();
|
|
|
|
if (!std::isfinite(radius) || radius <= 100.0 * std::numeric_limits<double>::epsilon()) {
|
|
return;
|
|
}
|
|
|
|
double radialGradient = 0.0;
|
|
|
|
if (transformation.Attribute == m_vacuumAttribute) {
|
|
/*
|
|
* In the compactified exterior, the three-dimensional H(div)
|
|
* Piola pullback of the inverse-square monopole field reduces
|
|
* to this finite computational-space expression.
|
|
*/
|
|
radialGradient = mean_field::utils::G * m_targetMass / (radius * radius);
|
|
} else {
|
|
const double pi = std::acos(-1.0);
|
|
const double xi = pi * radius / m_stellarRadius;
|
|
|
|
if (std::abs(xi) < 1.0e-5) {
|
|
/*
|
|
* sin(xi) - xi cos(xi) = xi^3 / 3 + O(xi^5).
|
|
*/
|
|
radialGradient = (4.0 / 3.0) * pi * mean_field::utils::G * m_centralDensity * radius;
|
|
} else {
|
|
radialGradient = 2.0 * m_polytropicConstant * m_centralDensity * pi / m_stellarRadius *
|
|
(std::sin(xi) - xi * std::cos(xi)) / (xi * xi);
|
|
}
|
|
}
|
|
|
|
value = computationalPosition;
|
|
value *= radialGradient / radius;
|
|
}
|
|
|
|
private:
|
|
int m_vacuumAttribute;
|
|
double m_stellarRadius;
|
|
double m_centralDensity;
|
|
double m_targetMass;
|
|
double m_polytropicConstant;
|
|
};
|
|
|
|
mean_field::utils::Args args = test_utils::setup_args();
|
|
|
|
mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0);
|
|
|
|
REQUIRE(f.okay());
|
|
REQUIRE(f.domainMapperStateless != nullptr);
|
|
REQUIRE(f.domainMapperStateless != nullptr);
|
|
|
|
*f.displacement = 0.0;
|
|
|
|
const double pi = std::acos(-1.0);
|
|
const double stellarRadius = mean_field::utils::RADIUS;
|
|
const double targetMass = mean_field::utils::MASS;
|
|
|
|
/*
|
|
* For an n = 1 Lane-Emden polytrope,
|
|
*
|
|
* R = sqrt(pi K / (2 G)),
|
|
*
|
|
* so choosing K this way places the analytic surface exactly at the
|
|
* stellar boundary of the mesh.
|
|
*/
|
|
const double polytropicConstant = 2.0 * mean_field::utils::G * stellarRadius * stellarRadius / pi;
|
|
|
|
/*
|
|
* The analytic n = 1 mass is
|
|
*
|
|
* M = 4 rho_c R^3 / pi.
|
|
*/
|
|
const double centralDensity = pi * targetMass / (4.0 * stellarRadius * stellarRadius * stellarRadius);
|
|
|
|
const double bernoulliConstant = -mean_field::utils::G * targetMass / stellarRadius;
|
|
|
|
const mean_field::eos::Polytrope barotrope(1.0, polytropicConstant);
|
|
|
|
const auto densityFunction = [centralDensity, stellarRadius, pi](const mfem::Vector &position) {
|
|
const double radius = position.Norml2();
|
|
|
|
if (radius >= stellarRadius) {
|
|
return 0.0;
|
|
}
|
|
|
|
const double xi = pi * radius / stellarRadius;
|
|
|
|
if (std::abs(xi) < 100.0 * std::numeric_limits<double>::epsilon()) {
|
|
return centralDensity;
|
|
}
|
|
|
|
return centralDensity * std::sin(xi) / xi;
|
|
};
|
|
|
|
const auto enthalpyFunction = [centralDensity, stellarRadius, polytropicConstant,
|
|
pi](const mfem::Vector &position) {
|
|
const double radius = position.Norml2();
|
|
|
|
if (radius >= stellarRadius) {
|
|
return 0.0;
|
|
}
|
|
|
|
const double xi = pi * radius / stellarRadius;
|
|
|
|
const double density = std::abs(xi) < 100.0 * std::numeric_limits<double>::epsilon()
|
|
? centralDensity
|
|
: centralDensity * std::sin(xi) / xi;
|
|
|
|
return 2.0 * polytropicConstant * density;
|
|
};
|
|
|
|
const auto potentialFunction = [centralDensity, stellarRadius, targetMass, polytropicConstant, bernoulliConstant,
|
|
pi](const mfem::Vector &physicalPosition) {
|
|
const double radius = physicalPosition.Norml2();
|
|
|
|
/*
|
|
* Phi tends to zero at compactified infinity.
|
|
*/
|
|
if (!std::isfinite(radius)) {
|
|
return 0.0;
|
|
}
|
|
|
|
if (radius >= stellarRadius) {
|
|
return radius > 0.0 ? -mean_field::utils::G * targetMass / radius : 0.0;
|
|
}
|
|
|
|
const double xi = pi * radius / stellarRadius;
|
|
|
|
const double density = std::abs(xi) < 100.0 * std::numeric_limits<double>::epsilon()
|
|
? centralDensity
|
|
: centralDensity * std::sin(xi) / xi;
|
|
|
|
const double enthalpy = 2.0 * polytropicConstant * density;
|
|
|
|
/*
|
|
* Hydrostatic equilibrium is h + Phi = C.
|
|
*/
|
|
return bernoulliConstant - enthalpy;
|
|
};
|
|
|
|
mfem::FunctionCoefficient densityCoefficient(densityFunction);
|
|
mfem::FunctionCoefficient enthalpyCoefficient(enthalpyFunction);
|
|
|
|
mean_field::mapping::PhysicalPositionFunctionCoefficient potentialCoefficient(
|
|
*f.domainMapperStateless, *f.displacement, *f.compactificationCoordinate, potentialFunction
|
|
);
|
|
|
|
LaneEmdenGravityGradientCoefficient gravityGradientCoefficient(
|
|
f.mesh->Dimension(), field_dof_test_utils::vacuum_material_attribute, stellarRadius, centralDensity, targetMass,
|
|
polytropicConstant
|
|
);
|
|
|
|
mfem::ParGridFunction densityField(f.densityFes.get());
|
|
mfem::ParGridFunction enthalpyField(f.enthalpyFes.get());
|
|
mfem::ParGridFunction gravityPotentialField(f.gravityPotentialFes.get());
|
|
mfem::ParGridFunction gravityGradientField(f.gravityFluxFes.get());
|
|
|
|
densityField = 0.0;
|
|
enthalpyField = 0.0;
|
|
gravityPotentialField = 0.0;
|
|
gravityGradientField = 0.0;
|
|
|
|
densityField.ProjectCoefficient(densityCoefficient);
|
|
enthalpyField.ProjectCoefficient(enthalpyCoefficient);
|
|
gravityPotentialField.ProjectCoefficient(potentialCoefficient);
|
|
gravityGradientField.ProjectCoefficient(gravityGradientCoefficient);
|
|
|
|
mfem::Vector densityTrue;
|
|
mfem::Vector enthalpyTrue;
|
|
mfem::Vector gravityPotentialTrue;
|
|
mfem::Vector gravityGradientTrue;
|
|
|
|
densityField.GetTrueDofs(densityTrue);
|
|
enthalpyField.GetTrueDofs(enthalpyTrue);
|
|
gravityPotentialField.GetTrueDofs(gravityPotentialTrue);
|
|
gravityGradientField.GetTrueDofs(gravityGradientTrue);
|
|
|
|
const auto stellarModel = stellar_equilibrium_test_utils::make_stellar_model(barotrope, targetMass);
|
|
|
|
mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator(
|
|
f, *f.domainMapperStateless, stellarModel
|
|
);
|
|
|
|
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);
|
|
|
|
/*
|
|
* Return to the analytic spherical state before testing the reduced
|
|
* mechanical dual and its rotational shape response.
|
|
*/
|
|
stellar_equilibrium_test_utils::increment_all_state_revisions(dependencies);
|
|
stellarOperator.Prepare(analyticState, dependencies, zeroRotation);
|
|
|
|
auto parameterGeometry = stellarModel.compileDomainDeformation(f);
|
|
const auto &surface = parameterGeometry.surfaceDeformationPrescription();
|
|
|
|
mfem::Vector oblateSurfaceDirection(surface.parameterCount());
|
|
for (int parameter = 0; parameter < surface.parameterCount(); ++parameter) {
|
|
const double polarDirection = surface.radialDirection(parameter, 2);
|
|
oblateSurfaceDirection(parameter) =
|
|
surface.referenceRadius(parameter) * (1.0 - 3.0 * polarDirection * polarDirection);
|
|
}
|
|
|
|
const mfem::Vector surfaceParameters = stellar_equilibrium_test_utils::const_value_view(
|
|
analyticState, layout, stellar_equilibrium_test_utils::displacementValue
|
|
);
|
|
mfem::Vector liftedOblateDirection(parameterGeometry.volumeDisplacementSize());
|
|
parameterGeometry.applyJacobian(surfaceParameters, oblateSurfaceDirection, liftedOblateDirection);
|
|
|
|
mfem::Vector nonrotatingResidual;
|
|
stellarOperator.BuildResidual(nonrotatingResidual);
|
|
const mfem::Vector nonrotatingShapeResidual = stellar_equilibrium_test_utils::const_residual_view(
|
|
nonrotatingResidual, layout, stellar_equilibrium_test_utils::displacementResidual
|
|
);
|
|
|
|
const double reducedVirtualWork =
|
|
gravity_prepared_test_utils::global_dot(oblateSurfaceDirection, nonrotatingShapeResidual, f.mesh->GetComm());
|
|
const double volumeVirtualWork = gravity_prepared_test_utils::global_dot(
|
|
liftedOblateDirection, stellarOperator.GetFullMechanicalResidual(), f.mesh->GetComm()
|
|
);
|
|
const double virtualWorkScale = std::max({1.0, std::abs(reducedVirtualWork), std::abs(volumeVirtualWork)});
|
|
|
|
INFO("Reduced mechanical virtual work = " << reducedVirtualWork);
|
|
INFO("Lifted volume mechanical virtual work = " << volumeVirtualWork);
|
|
CHECK(std::abs(reducedVirtualWork - volumeVirtualWork) <= 2.0e-12 * virtualWorkScale);
|
|
|
|
const double keplerianAngularSpeed =
|
|
std::sqrt(mean_field::utils::G * targetMass / (stellarRadius * stellarRadius * stellarRadius));
|
|
const double angularSpeed = 0.25 * 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);
|
|
|
|
++dependencies.rotation.revision;
|
|
stellarOperator.Prepare(analyticState, dependencies, rotation);
|
|
|
|
mfem::Vector rotatingSphericalResidual;
|
|
stellarOperator.BuildResidual(rotatingSphericalResidual);
|
|
const mfem::Vector rotatingShapeResidual = stellar_equilibrium_test_utils::const_residual_view(
|
|
rotatingSphericalResidual, layout, stellar_equilibrium_test_utils::displacementResidual
|
|
);
|
|
mfem::Vector rotationInducedShapeResidual(rotatingShapeResidual);
|
|
rotationInducedShapeResidual -= nonrotatingShapeResidual;
|
|
|
|
const double rotationInducedWork = gravity_prepared_test_utils::global_dot(
|
|
rotationInducedShapeResidual, oblateSurfaceDirection, f.mesh->GetComm()
|
|
);
|
|
const double rotationInducedNorm =
|
|
stellar_equilibrium_test_utils::global_norm(rotationInducedShapeResidual, f.mesh->GetComm());
|
|
const double oblateDirectionNorm =
|
|
stellar_equilibrium_test_utils::global_norm(oblateSurfaceDirection, f.mesh->GetComm());
|
|
const double rotationWorkScale = rotationInducedNorm * oblateDirectionNorm;
|
|
|
|
INFO("Rotation-induced reduced shape residual norm = " << rotationInducedNorm);
|
|
INFO("Rotation-induced work against the oblate surface direction = " << rotationInducedWork);
|
|
REQUIRE(rotationInducedNorm > 0.0);
|
|
REQUIRE(oblateDirectionNorm > 0.0);
|
|
REQUIRE(rotationWorkScale > 0.0);
|
|
CHECK(rotationInducedWork < -1.0e-3 * rotationWorkScale);
|
|
|
|
mfem::Vector oblateDirection(layout.value_offsets().Last());
|
|
oblateDirection = 0.0;
|
|
stellar_equilibrium_test_utils::assign_value_block(
|
|
oblateDirection, layout, stellar_equilibrium_test_utils::displacementValue, oblateSurfaceDirection
|
|
);
|
|
|
|
const auto deformationStatisticsBefore = stellarOperator.GetDomainDeformation().actionStatistics();
|
|
mfem::Vector oblateJacobianAction;
|
|
stellarOperator.Mult(oblateDirection, oblateJacobianAction);
|
|
const auto deformationStatisticsAfter = stellarOperator.GetDomainDeformation().actionStatistics();
|
|
CHECK(
|
|
deformationStatisticsAfter.pullbackDerivativeApplications ==
|
|
deformationStatisticsBefore.pullbackDerivativeApplications + 1
|
|
);
|
|
|
|
const mfem::Vector oblateShapeJacobianAction = stellar_equilibrium_test_utils::const_residual_view(
|
|
oblateJacobianAction, layout, stellar_equilibrium_test_utils::displacementResidual
|
|
);
|
|
/*
|
|
* Test descent for the rotation-induced departure from the nonrotating
|
|
* state. The analytic finite-element state has a nonzero projection floor,
|
|
* so minimizing the absolute residual would mix that unrelated floor into
|
|
* the rotational response. The sign of the best correction along this
|
|
* single trial coordinate is deliberately not prescribed: a shape-only
|
|
* correction holds the thermodynamic and gravity unknowns fixed, whereas
|
|
* the physical oblate equilibrium is a coupled response of every block.
|
|
*/
|
|
const double residualDirectionalDerivative = gravity_prepared_test_utils::global_dot(
|
|
rotationInducedShapeResidual, oblateShapeJacobianAction, f.mesh->GetComm()
|
|
);
|
|
const double jacobianDirectionNormSquared = gravity_prepared_test_utils::global_dot(
|
|
oblateShapeJacobianAction, oblateShapeJacobianAction, f.mesh->GetComm()
|
|
);
|
|
|
|
REQUIRE(std::isfinite(residualDirectionalDerivative));
|
|
REQUIRE(jacobianDirectionNormSquared > 0.0);
|
|
REQUIRE(std::abs(residualDirectionalDerivative) > 1.0e-12 * rotationWorkScale);
|
|
|
|
const double optimalLinearizedAmplitude = -residualDirectionalDerivative / jacobianDirectionNormSquared;
|
|
const double appliedShapeAmplitude =
|
|
std::copysign(std::min(0.25 * std::abs(optimalLinearizedAmplitude), 2.0e-2), optimalLinearizedAmplitude);
|
|
REQUIRE(appliedShapeAmplitude != 0.0);
|
|
|
|
mfem::Vector predictedShapeResidual(rotationInducedShapeResidual);
|
|
predictedShapeResidual.Add(appliedShapeAmplitude, oblateShapeJacobianAction);
|
|
const double predictedShapeNorm =
|
|
stellar_equilibrium_test_utils::global_norm(predictedShapeResidual, f.mesh->GetComm());
|
|
CHECK(predictedShapeNorm < rotationInducedNorm);
|
|
|
|
mfem::Vector correctedShapeState(analyticState);
|
|
stellar_equilibrium_test_utils::value_view(
|
|
correctedShapeState, layout, stellar_equilibrium_test_utils::displacementValue
|
|
)
|
|
.Add(appliedShapeAmplitude, oblateSurfaceDirection);
|
|
++dependencies.surfaceDeformation.revision;
|
|
stellarOperator.Prepare(correctedShapeState, dependencies, rotation);
|
|
|
|
mfem::Vector nonlinearCorrectedResidual;
|
|
stellarOperator.BuildResidual(nonlinearCorrectedResidual);
|
|
mfem::Vector nonlinearShapeDeparture(
|
|
stellar_equilibrium_test_utils::const_residual_view(
|
|
nonlinearCorrectedResidual, layout, stellar_equilibrium_test_utils::displacementResidual
|
|
)
|
|
);
|
|
nonlinearShapeDeparture -= nonrotatingShapeResidual;
|
|
const double nonlinearShapeDepartureNorm =
|
|
stellar_equilibrium_test_utils::global_norm(nonlinearShapeDeparture, f.mesh->GetComm());
|
|
|
|
INFO("Optimal linearized shape amplitude = " << optimalLinearizedAmplitude);
|
|
INFO("Applied shape amplitude = " << appliedShapeAmplitude);
|
|
INFO("Rotation-induced shape residual norm = " << rotationInducedNorm);
|
|
INFO("Predicted corrected rotational departure norm = " << predictedShapeNorm);
|
|
INFO("Nonlinear corrected rotational departure norm = " << nonlinearShapeDepartureNorm);
|
|
CHECK(nonlinearShapeDepartureNorm < rotationInducedNorm);
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Prepared Stellar Equilibrium Has A Restoring Jacobian Around An N3 "
|
|
"Polytrope",
|
|
tags::barotrope &tags::prepared &tags::analytic_comparison &tags::accuracy &tags::gravity &tags::hydro
|
|
&tags::jacobian &tags::convergence
|
|
) {
|
|
mean_field::utils::Args args = test_utils::setup_args();
|
|
|
|
args.p.rtol = 1.0e-12;
|
|
args.p.max_iters = std::max(args.p.max_iters, 1000);
|
|
|
|
mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0);
|
|
|
|
REQUIRE(f.okay());
|
|
REQUIRE(f.domainMapperStateless != nullptr);
|
|
REQUIRE(f.domainMapperStateless != nullptr);
|
|
|
|
const double pi = std::acos(-1.0);
|
|
const double stellarRadius = mean_field::utils::RADIUS;
|
|
const double targetMass = mean_field::utils::MASS;
|
|
|
|
/*
|
|
* Standard n = 3 Lane-Emden constants:
|
|
*
|
|
* xi_1 = 6.896848619...
|
|
* -xi_1^2 theta'(xi_1) = 2.018235951...
|
|
*/
|
|
constexpr double surfaceCoordinate = 6.8968486193769603755;
|
|
|
|
constexpr double dimensionlessMass = 2.0182359509662283534;
|
|
|
|
/*
|
|
* For n = 3,
|
|
*
|
|
* M = 4 pi (K / (pi G))^(3/2) mu_1.
|
|
*
|
|
* This fixes K for the requested target mass.
|
|
*/
|
|
const double polytropicConstant =
|
|
pi * mean_field::utils::G * std::pow(targetMass / (4.0 * pi * dimensionlessMass), 2.0 / 3.0);
|
|
|
|
/*
|
|
* The n = 3 radius is
|
|
*
|
|
* R = xi_1 sqrt(K / (pi G)) rho_c^(-1/3).
|
|
*
|
|
* Choose rho_c so that the Lane-Emden surface coincides with the
|
|
* stellar boundary of the test mesh.
|
|
*/
|
|
const double centralDensity =
|
|
std::pow(surfaceCoordinate * std::sqrt(polytropicConstant / (pi * mean_field::utils::G)) / stellarRadius, 3.0);
|
|
|
|
const mean_field::eos::Polytrope equationOfState(3.0, polytropicConstant);
|
|
|
|
const mean_field::models::structure::PolytropicStructure structurePrescription(equationOfState, targetMass);
|
|
|
|
const mean_field::models::structure::StructureSeed seed =
|
|
structurePrescription.makeInitialSeed({.centralDensity = centralDensity, .radialSampleCount = 8192});
|
|
|
|
INFO("Requested stellar radius = " << stellarRadius);
|
|
INFO("Seed stellar radius = " << seed.stellarRadius);
|
|
INFO("Target mass = " << targetMass);
|
|
INFO("Polytropic constant = " << polytropicConstant);
|
|
INFO("Central density = " << centralDensity);
|
|
|
|
REQUIRE(seed.radius.Size() == seed.density.Size());
|
|
REQUIRE(seed.radius.Size() == seed.enthalpy.Size());
|
|
REQUIRE(seed.radius.Size() == 8192);
|
|
|
|
CHECK(std::abs(seed.stellarRadius - stellarRadius) / stellarRadius < 2.0e-4);
|
|
|
|
const auto interpolateProfile = [](const mfem::Vector &radiusSamples, const mfem::Vector &valueSamples,
|
|
const double radius) {
|
|
MFEM_VERIFY(radiusSamples.Size() == valueSamples.Size(), "The radial profile has inconsistent sample sizes.");
|
|
|
|
MFEM_VERIFY(radiusSamples.Size() >= 2, "The radial profile requires at least two samples.");
|
|
|
|
if (radius <= radiusSamples(0)) {
|
|
return valueSamples(0);
|
|
}
|
|
|
|
const int finalIndex = radiusSamples.Size() - 1;
|
|
|
|
if (radius >= radiusSamples(finalIndex)) {
|
|
return valueSamples(finalIndex);
|
|
}
|
|
|
|
int lowerIndex = 0;
|
|
int upperIndex = finalIndex;
|
|
|
|
while (upperIndex - lowerIndex > 1) {
|
|
const int middleIndex = lowerIndex + (upperIndex - lowerIndex) / 2;
|
|
|
|
if (radiusSamples(middleIndex) <= radius) {
|
|
lowerIndex = middleIndex;
|
|
} else {
|
|
upperIndex = middleIndex;
|
|
}
|
|
}
|
|
|
|
const double radialInterval = radiusSamples(upperIndex) - radiusSamples(lowerIndex);
|
|
|
|
MFEM_VERIFY(radialInterval > 0.0, "The radial profile is not strictly increasing.");
|
|
|
|
const double fraction = (radius - radiusSamples(lowerIndex)) / radialInterval;
|
|
|
|
return (1.0 - fraction) * valueSamples(lowerIndex) + fraction * valueSamples(upperIndex);
|
|
};
|
|
|
|
mfem::FunctionCoefficient densityCoefficient([&seed, &interpolateProfile](const mfem::Vector &position) {
|
|
const double radius = position.Norml2();
|
|
|
|
if (radius >= seed.stellarRadius) {
|
|
return 0.0;
|
|
}
|
|
|
|
return interpolateProfile(seed.radius, seed.density, radius);
|
|
});
|
|
|
|
mfem::FunctionCoefficient enthalpyCoefficient([&seed, &interpolateProfile](const mfem::Vector &position) {
|
|
const double radius = position.Norml2();
|
|
|
|
if (radius >= seed.stellarRadius) {
|
|
return 0.0;
|
|
}
|
|
|
|
return interpolateProfile(seed.radius, seed.enthalpy, radius);
|
|
});
|
|
|
|
mfem::ParGridFunction densityField(f.densityFes.get());
|
|
mfem::ParGridFunction enthalpyField(f.enthalpyFes.get());
|
|
mfem::ParGridFunction displacementField(f.displacementFes.get());
|
|
|
|
densityField = 0.0;
|
|
enthalpyField = 0.0;
|
|
displacementField = 0.0;
|
|
|
|
densityField.ProjectCoefficient(densityCoefficient);
|
|
enthalpyField.ProjectCoefficient(enthalpyCoefficient);
|
|
|
|
/*
|
|
* Gravity initialization and the prepared root operator must see the
|
|
* same undeformed geometry.
|
|
*/
|
|
*f.displacement = displacementField;
|
|
|
|
const mean_field::physics::GravitySolution gravitySolution =
|
|
mean_field::physics::solve_gravity_field(f, args, densityField, displacementField);
|
|
|
|
mfem::Vector densityTrue;
|
|
mfem::Vector enthalpyTrue;
|
|
mfem::Vector displacementTrue;
|
|
mfem::Vector gravityGradientTrue;
|
|
mfem::Vector gravityPotentialTrue;
|
|
|
|
densityField.GetTrueDofs(densityTrue);
|
|
enthalpyField.GetTrueDofs(enthalpyTrue);
|
|
displacementField.GetTrueDofs(displacementTrue);
|
|
gravitySolution.gradPhi.GetTrueDofs(gravityGradientTrue);
|
|
gravitySolution.phi.GetTrueDofs(gravityPotentialTrue);
|
|
|
|
const double bernoulliConstant = -mean_field::utils::G * targetMass / stellarRadius;
|
|
|
|
const mean_field::eos::Polytrope barotrope(3.0, polytropicConstant);
|
|
|
|
const auto stellarModel = stellar_equilibrium_test_utils::make_stellar_model(barotrope, targetMass);
|
|
|
|
mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator(
|
|
f, *f.domainMapperStateless, stellarModel
|
|
);
|
|
|
|
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 auto unconstrainedHydrostaticNorm = [&stellarOperator, &layout, &f](const mfem::Vector &residual) {
|
|
const mfem::Vector hydrostaticResidual = stellar_equilibrium_test_utils::const_residual_view(
|
|
residual, layout, stellar_equilibrium_test_utils::enthalpyResidual
|
|
);
|
|
mfem::Vector volumeHydrostaticResidual(hydrostaticResidual.Size());
|
|
volumeHydrostaticResidual = hydrostaticResidual;
|
|
for (const int constrainedRow :
|
|
stellarOperator.GetSurfaceConstraintOperator().GetSurfaceRows().reduced_dofs()) {
|
|
volumeHydrostaticResidual(constrainedRow) = 0.0;
|
|
}
|
|
return stellar_equilibrium_test_utils::global_norm(volumeHydrostaticResidual, f.mesh->GetComm());
|
|
};
|
|
|
|
const auto pressureSurfaceNorm = [&stellarOperator, &layout, &f](const mfem::Vector &residual) {
|
|
const mfem::Vector hydrostaticResidual = stellar_equilibrium_test_utils::const_residual_view(
|
|
residual, layout, stellar_equilibrium_test_utils::enthalpyResidual
|
|
);
|
|
double localNormSquared = 0.0;
|
|
for (const int constrainedRow :
|
|
stellarOperator.GetSurfaceConstraintOperator().GetSurfaceRows().reduced_dofs()) {
|
|
localNormSquared += hydrostaticResidual(constrainedRow) * hydrostaticResidual(constrainedRow);
|
|
}
|
|
double globalNormSquared = 0.0;
|
|
MPI_Allreduce(&localNormSquared, &globalNormSquared, 1, MPI_DOUBLE, MPI_SUM, f.mesh->GetComm());
|
|
return std::sqrt(globalNormSquared);
|
|
};
|
|
|
|
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),
|
|
unconstrainedHydrostaticNorm(equilibriumResidual),
|
|
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),
|
|
unconstrainedHydrostaticNorm(perturbedResidual),
|
|
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 equilibriumPressureSurfaceNorm = pressureSurfaceNorm(equilibriumResidual);
|
|
const double perturbedPressureSurfaceNorm = pressureSurfaceNorm(perturbedResidual);
|
|
INFO("Equilibrium pressure-surface projection floor = " << equilibriumPressureSurfaceNorm);
|
|
INFO("Perturbed pressure-surface residual norm = " << perturbedPressureSurfaceNorm);
|
|
CHECK(equilibriumPressureSurfaceNorm < perturbedPressureSurfaceNorm);
|
|
CHECK(equilibriumPressureSurfaceNorm < 5.0e-4);
|
|
|
|
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.surfaceDeformation.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);
|
|
}
|
|
}
|