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

1215 lines
45 KiB
C++

#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <sstream>
#include <mfem.hpp>
#include <mpi.h>
import mean_field;
import test_helpers;
namespace {
constexpr std::array<double, 6> shell_boundaries{0.0, 0.25, 0.50,
0.75, 0.90, 1.0};
constexpr int shell_count = static_cast<int>(shell_boundaries.size()) - 1;
struct ShellAccumulator {
long long points{0};
double weight{0.0};
double minimum_radius{std::numeric_limits<double>::infinity()};
double maximum_radius{0.0};
double potential_error_squared{0.0};
double radial_error_squared{0.0};
double tangential_squared{0.0};
};
struct ShellMetrics {
long long points{0};
double minimum_radius{0.0};
double maximum_radius{0.0};
double potential_rms_error{0.0};
double radial_rms_error{0.0};
double tangential_rms{0.0};
};
struct ShellMeasurement {
std::array<ShellMetrics, shell_count> shells{};
long long invalid_points{0};
};
constexpr int mapping_status_count = 8;
struct GravitationalEnergies {
double binding{0.0};
double virial{0.0};
double minimum_mapping_determinant{std::numeric_limits<double>::infinity()};
double maximum_mapping_determinant{-std::numeric_limits<double>::infinity()};
long long invalid_points{0};
std::array<long long, mapping_status_count> mapping_status_counts{};
int first_invalid_element{-1};
int first_invalid_attribute{-1};
int first_invalid_quadrature_point{-1};
double first_invalid_determinant{std::numeric_limits<double>::quiet_NaN()};
};
constexpr int
mapping_status_index(const mean_field::mapping::MappingStatus status) {
return static_cast<int>(status);
}
void zero_vacuum_density(const mean_field::fem::FEM &f,
mfem::GridFunction &density) {
using DomainSchema =
mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
const mean_field::field::FieldDofMap densityMap =
mean_field::field::make_field_dof_map<mean_field::field::Density,
DomainSchema>(*f.densityFes);
mfem::Vector densityTrue;
density.GetTrueDofs(densityTrue);
const mfem::Vector supportedDensity = densityMap.gather(densityTrue);
densityMap.scatter(supportedDensity, densityTrue);
density.SetFromTrueDofs(densityTrue);
}
double global_norm(const mfem::Vector &vector, MPI_Comm communicator) {
const double local_norm_squared = vector * vector;
double global_norm_squared = 0.0;
MPI_Allreduce(&local_norm_squared, &global_norm_squared, 1, MPI_DOUBLE,
MPI_SUM, communicator);
return std::sqrt(global_norm_squared);
}
double global_dot(const mfem::Vector &lhs, const mfem::Vector &rhs,
MPI_Comm communicator) {
const double local_dot = lhs * rhs;
double result = 0.0;
MPI_Allreduce(&local_dot, &result, 1, MPI_DOUBLE, MPI_SUM, communicator);
return result;
}
double global_relative_error(const mfem::Vector &computed,
const mfem::Vector &reference,
MPI_Comm communicator) {
REQUIRE(computed.Size() == reference.Size());
mfem::Vector difference(computed);
difference -= reference;
return global_norm(difference, communicator) /
std::max(global_norm(reference, communicator),
std::numeric_limits<double>::epsilon());
}
int get_shell(const double coordinate) {
const double clamped = std::clamp(coordinate, 0.0, std::nextafter(1.0, 0.0));
for (int shell = 0; shell < shell_count; ++shell) {
if (clamped < shell_boundaries[shell + 1]) {
return shell;
}
}
return shell_count - 1;
}
bool retryable_infinity_status(
const mean_field::mapping::MappingStatus status) {
return status ==
mean_field::mapping::MappingStatus::at_compactified_infinity ||
status ==
mean_field::mapping::MappingStatus::outside_reference_domain ||
status == mean_field::mapping::MappingStatus::non_finite_result ||
status == mean_field::mapping::MappingStatus::non_positive_determinant;
}
class ProjectionGeometry {
public:
ProjectionGeometry(const mean_field::fem::FEM &f,
const mean_field::mapping::DomainMapper &mapper,
const mfem::GridFunction &displacement)
: m_fem(f), m_mapper(mapper), m_displacement(displacement),
m_workspace(f.mesh->Dimension()) {}
mean_field::mapping::MappingStatus
Evaluate(mfem::ElementTransformation &transformation,
const mfem::IntegrationPoint &integration_point,
mean_field::mapping::MappingPointContext &context,
const bool permit_infinity_limit) {
m_used_infinity_limit = false;
const int element_id = transformation.ElementNo;
MFEM_VERIFY(element_id >= 0,
"Projection coefficient received an invalid element number.");
const mfem::FiniteElement &displacement_element =
*m_fem.displacementFes->GetFE(element_id);
const mfem::FiniteElement &compactification_element =
*m_fem.compactificationFes->GetFE(element_id);
mfem::Array<int> displacement_dofs;
mfem::Array<int> compactification_dofs;
mfem::DofTransformation *displacement_transform =
m_fem.displacementFes->GetElementVDofs(element_id, displacement_dofs);
mfem::DofTransformation *compactification_transform =
m_fem.compactificationFes->GetElementDofs(element_id,
compactification_dofs);
mfem::Vector element_displacement;
mfem::Vector element_compactification;
m_displacement.GetSubVector(displacement_dofs, element_displacement);
m_fem.compactificationCoordinate->GetSubVector(compactification_dofs,
element_compactification);
if (displacement_transform != nullptr) {
displacement_transform->InvTransformPrimal(element_displacement);
}
if (compactification_transform != nullptr) {
compactification_transform->InvTransformPrimal(element_compactification);
}
const mean_field::mapping::ElementDisplacementData displacement_data =
mean_field::mapping::ElementDisplacementDataFromElementVDofs(
displacement_element, element_displacement);
const mean_field::mapping::ElementCompactificationData
compactification_data(compactification_element,
element_compactification);
const mean_field::mapping::ElementMappingData mapping_data{
.displacement = displacement_data,
.compactification = compactification_data};
mfem::Vector compactification_shape(compactification_element.GetDof());
compactification_element.CalcShape(integration_point,
compactification_shape);
const double coordinate = element_compactification * compactification_shape;
mean_field::mapping::MappingStatus status = m_mapper.EvaluatePoint(
mapping_data, transformation, integration_point, m_workspace, context);
if (status == mean_field::mapping::MappingStatus::valid) {
transformation.SetIntPoint(&integration_point);
return status;
}
const bool infinity_request =
m_mapper.IsCompactifiedElement(transformation) &&
coordinate >= 1.0 - 1.0e-10;
if (!permit_infinity_limit || !infinity_request ||
!retryable_infinity_status(status)) {
transformation.SetIntPoint(&integration_point);
return status;
}
const mfem::IntegrationPoint &center =
mfem::Geometries.GetCenter(transformation.GetGeometryType());
constexpr std::array<double, 11> inward_fractions{
1.0e-12, 1.0e-11, 1.0e-10, 1.0e-9, 1.0e-8, 1.0e-7,
1.0e-6, 1.0e-5, 1.0e-4, 1.0e-3, 1.0e-2};
for (const double fraction : inward_fractions) {
mfem::IntegrationPoint inward;
inward.x = (1.0 - fraction) * integration_point.x + fraction * center.x;
inward.y = (1.0 - fraction) * integration_point.y + fraction * center.y;
inward.z = (1.0 - fraction) * integration_point.z + fraction * center.z;
inward.weight = integration_point.weight;
status = m_mapper.EvaluatePoint(mapping_data, transformation, inward,
m_workspace, context);
if (status == mean_field::mapping::MappingStatus::valid) {
m_used_infinity_limit = true;
transformation.SetIntPoint(&integration_point);
return status;
}
if (!retryable_infinity_status(status)) {
break;
}
}
transformation.SetIntPoint(&integration_point);
return status;
}
[[nodiscard]] bool UsedInfinityLimit() const noexcept {
return m_used_infinity_limit;
}
private:
const mean_field::fem::FEM &m_fem;
const mean_field::mapping::DomainMapper &m_mapper;
const mfem::GridFunction &m_displacement;
mean_field::mapping::DomainMapper::Workspace m_workspace;
bool m_used_infinity_limit{false};
};
class MonopolePotentialCoefficient final : public mfem::Coefficient {
public:
MonopolePotentialCoefficient(
const mean_field::fem::FEM &f,
const mean_field::mapping::DomainMapper &mapper,
const mfem::GridFunction &displacement, const double mass,
const double radius)
: m_geometry(f, mapper, displacement),
m_vacuum_attribute(field_dof_test_utils::vacuum_material_attribute),
m_mass(mass), m_radius(radius) {}
double Eval(mfem::ElementTransformation &transformation,
const mfem::IntegrationPoint &integration_point) override {
mean_field::mapping::MappingPointContext context;
const mean_field::mapping::MappingStatus status =
m_geometry.Evaluate(transformation, integration_point, context, true);
MFEM_VERIFY(status == mean_field::mapping::MappingStatus::valid,
"Stateless monopole-potential projection failed with status "
<< static_cast<int>(status) << " on element "
<< transformation.ElementNo << '.');
if (m_geometry.UsedInfinityLimit()) {
return 0.0;
}
const double radius = context.physical_position.Norml2();
MFEM_VERIFY(std::isfinite(radius) && radius > 0.0,
"Invalid monopole projection radius.");
if (transformation.Attribute == m_vacuum_attribute) {
return -mean_field::utils::G * m_mass / radius;
}
return -mean_field::utils::G * m_mass *
(3.0 * m_radius * m_radius - radius * radius) /
(2.0 * m_radius * m_radius * m_radius);
}
private:
ProjectionGeometry m_geometry;
int m_vacuum_attribute;
double m_mass;
double m_radius;
};
void local_to_true(const mfem::ParFiniteElementSpace &space,
const mfem::Vector &local, mfem::Vector &true_vector) {
true_vector.SetSize(space.GetTrueVSize());
true_vector = 0.0;
const mfem::Operator *prolongation = space.GetProlongationMatrix();
if (prolongation != nullptr) {
prolongation->MultTranspose(local, true_vector);
} else {
true_vector = local;
}
}
mfem::Vector assemble_monopole_projection_rhs(
mean_field::fem::FEM &f, const mfem::GridFunction &displacement,
const double mass, const double stellar_radius) {
mfem::Vector local_rhs(f.gravityFluxFes->GetVSize());
local_rhs = 0.0;
mean_field::mapping::DomainMapper::Workspace workspace(
f.mesh->Dimension());
const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute;
const int quadrature_order = 2 * f.gravityFluxFes->GetMaxElementOrder() + 8;
for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) {
const mfem::FiniteElement &gravity_element =
*f.gravityFluxFes->GetFE(element_id);
const mfem::FiniteElement &displacement_element =
*f.displacementFes->GetFE(element_id);
const mfem::FiniteElement &compactification_element =
*f.compactificationFes->GetFE(element_id);
mfem::ElementTransformation *transformation =
f.mesh->GetElementTransformation(element_id);
mfem::Array<int> gravity_dofs;
mfem::Array<int> displacement_dofs;
mfem::Array<int> compactification_dofs;
mfem::DofTransformation *gravity_transform =
f.gravityFluxFes->GetElementVDofs(element_id, gravity_dofs);
mfem::DofTransformation *displacement_transform =
f.displacementFes->GetElementVDofs(element_id, displacement_dofs);
mfem::DofTransformation *compactification_transform =
f.compactificationFes->GetElementDofs(element_id,
compactification_dofs);
mfem::Vector element_displacement;
mfem::Vector element_compactification;
displacement.GetSubVector(displacement_dofs, element_displacement);
f.compactificationCoordinate->GetSubVector(compactification_dofs,
element_compactification);
if (displacement_transform != nullptr) {
displacement_transform->InvTransformPrimal(element_displacement);
}
if (compactification_transform != nullptr) {
compactification_transform->InvTransformPrimal(element_compactification);
}
const mean_field::mapping::ElementDisplacementData displacement_data =
mean_field::mapping::ElementDisplacementDataFromElementVDofs(
displacement_element, element_displacement);
const mean_field::mapping::ElementCompactificationData
compactification_data(compactification_element,
element_compactification);
const mean_field::mapping::ElementMappingData mapping_data{
.displacement = displacement_data,
.compactification = compactification_data};
const int dof_count = gravity_element.GetDof();
const int dimension = transformation->GetSpaceDim();
mfem::Vector element_rhs(dof_count);
mfem::Vector analytic_field(dimension);
mfem::Vector pulled_rhs_field(dimension);
mfem::DenseMatrix vector_shape(dof_count, dimension);
element_rhs = 0.0;
const mfem::IntegrationRule &rule =
mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order);
for (int q = 0; q < rule.GetNPoints(); ++q) {
const mfem::IntegrationPoint &point = rule.IntPoint(q);
mean_field::mapping::VolumeMappingContext context;
const mean_field::mapping::MappingStatus status =
f.domainMapperStateless->EvaluateVolume(mapping_data, *transformation,
point, workspace, context);
MFEM_VERIFY(status == mean_field::mapping::MappingStatus::valid,
"Mapped monopole projection RHS failed with status "
<< static_cast<int>(status) << " on element "
<< element_id << ", quadrature point " << q << '.');
analytic_field = context.mapping.physical_position;
const double radius = analytic_field.Norml2();
MFEM_VERIFY(std::isfinite(radius) && radius > 0.0,
"Invalid physical radius in projection RHS.");
if (transformation->Attribute == vacuum_attribute) {
analytic_field *=
mean_field::utils::G * mass / (radius * radius * radius);
} else {
analytic_field *= mean_field::utils::G * mass /
(stellar_radius * stellar_radius * stellar_radius);
}
context.mapping.mapping_jacobian.MultTranspose(analytic_field,
pulled_rhs_field);
transformation->SetIntPoint(&point);
gravity_element.CalcVShape(*transformation, vector_shape);
const double weight = point.weight * transformation->Weight();
for (int i = 0; i < dof_count; ++i) {
for (int component = 0; component < dimension; ++component) {
element_rhs(i) +=
weight * vector_shape(i, component) * pulled_rhs_field(component);
}
}
}
if (gravity_transform != nullptr) {
gravity_transform->TransformDual(element_rhs);
}
local_rhs.AddElementVector(gravity_dofs, element_rhs);
}
mfem::Vector true_rhs;
local_to_true(*f.gravityFluxFes, local_rhs, true_rhs);
return true_rhs;
}
mfem::Vector
project_monopole_gradient(mean_field::fem::FEM &f,
const mfem::ParGridFunction &displacement,
const double mass, const double stellar_radius) {
mfem::Vector displacement_true;
displacement.GetTrueDofs(displacement_true);
const mfem::Vector projection_rhs =
assemble_monopole_projection_rhs(f, displacement, mass, stellar_radius);
mean_field::operators::PreparedMappedHDivMassOperator mass_operator(
f, *f.domainMapperStateless);
mass_operator.Prepare(
mass_operator.GetDisplacementMap().gather(displacement_true));
const mfem::Vector reduced_projection_rhs =
mass_operator.GetFluxMap().gather(projection_rhs);
mfem::Vector projected_gradient(mass_operator.Width());
projected_gradient = 0.0;
mfem::CGSolver solver(f.gravityFluxFes->GetComm());
solver.SetOperator(mass_operator);
solver.SetRelTol(1.0e-9);
solver.SetAbsTol(1.0e-12);
solver.SetMaxIter(2000);
solver.SetPrintLevel(0);
solver.Mult(reduced_projection_rhs, projected_gradient);
mfem::Vector projection_residual;
mass_operator.Mult(projected_gradient, projection_residual);
projection_residual -= reduced_projection_rhs;
const double source_norm =
global_norm(reduced_projection_rhs, f.gravityFluxFes->GetComm());
const double residual_norm =
global_norm(projection_residual, f.gravityFluxFes->GetComm());
const double relative_residual =
residual_norm /
std::max(source_norm, std::numeric_limits<double>::epsilon());
INFO("Mapped H(div) projection converged = " << solver.GetConverged());
INFO("Mapped H(div) projection iterations = " << solver.GetNumIterations());
INFO("Mapped H(div) projection reported final norm = "
<< solver.GetFinalNorm());
INFO("Mapped H(div) projection direct residual norm = " << residual_norm);
INFO("Mapped H(div) projection direct relative residual = "
<< relative_residual);
REQUIRE(std::isfinite(relative_residual));
REQUIRE(relative_residual < 1.0e-8);
return mass_operator.GetFluxMap().scatter(projected_gradient);
}
double mapped_hdiv_relative_error(mean_field::fem::FEM &f,
const mfem::ParGridFunction &displacement,
const mfem::Vector &computed,
const mfem::Vector &reference) {
REQUIRE(computed.Size() == reference.Size());
mfem::Vector displacement_true;
displacement.GetTrueDofs(displacement_true);
mean_field::operators::PreparedMappedHDivMassOperator mass_operator(
f, *f.domainMapperStateless);
mass_operator.Prepare(
mass_operator.GetDisplacementMap().gather(displacement_true));
mfem::Vector difference(computed);
difference -= reference;
mfem::Vector difference_action;
mfem::Vector reference_action;
const mfem::Vector reduced_difference =
mass_operator.GetFluxMap().gather(difference);
const mfem::Vector reduced_reference =
mass_operator.GetFluxMap().gather(reference);
mass_operator.Mult(reduced_difference, difference_action);
mass_operator.Mult(reduced_reference, reference_action);
MPI_Comm communicator = f.gravityFluxFes->GetComm();
const double difference_energy =
global_dot(reduced_difference, difference_action, communicator);
const double reference_energy =
global_dot(reduced_reference, reference_action, communicator);
REQUIRE(difference_energy >= -1.0e-12 * std::abs(reference_energy));
REQUIRE(reference_energy > 0.0);
return std::sqrt(std::max(0.0, difference_energy) / reference_energy);
}
ShellMeasurement
measure_exterior_shells(mean_field::fem::FEM &f,
const mean_field::physics::GravitySolution &solution,
const mfem::GridFunction &displacement,
const double mass) {
std::array<ShellAccumulator, shell_count> local{};
long long local_invalid_points = 0;
mean_field::mapping::DomainMapper::Workspace workspace(
f.mesh->Dimension());
const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute;
const int quadrature_order =
2 * std::max(f.gravityPotentialFes->GetMaxElementOrder(),
f.gravityFluxFes->GetMaxElementOrder()) +
8;
for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) {
mfem::ElementTransformation *transformation =
f.mesh->GetElementTransformation(element_id);
if (transformation->Attribute != vacuum_attribute) {
continue;
}
const mfem::FiniteElement &displacement_element =
*f.displacementFes->GetFE(element_id);
const mfem::FiniteElement &compactification_element =
*f.compactificationFes->GetFE(element_id);
mfem::Array<int> displacement_dofs;
mfem::Array<int> compactification_dofs;
mfem::DofTransformation *displacement_transform =
f.displacementFes->GetElementVDofs(element_id, displacement_dofs);
mfem::DofTransformation *compactification_transform =
f.compactificationFes->GetElementDofs(element_id,
compactification_dofs);
mfem::Vector element_displacement;
mfem::Vector element_compactification;
displacement.GetSubVector(displacement_dofs, element_displacement);
f.compactificationCoordinate->GetSubVector(compactification_dofs,
element_compactification);
if (displacement_transform != nullptr) {
displacement_transform->InvTransformPrimal(element_displacement);
}
if (compactification_transform != nullptr) {
compactification_transform->InvTransformPrimal(element_compactification);
}
const mean_field::mapping::ElementDisplacementData displacement_data =
mean_field::mapping::ElementDisplacementDataFromElementVDofs(
displacement_element, element_displacement);
const mean_field::mapping::ElementCompactificationData
compactification_data(compactification_element,
element_compactification);
const mean_field::mapping::ElementMappingData mapping_data{
.displacement = displacement_data,
.compactification = compactification_data};
mfem::Vector compactification_shape(compactification_element.GetDof());
const mfem::IntegrationRule &rule =
mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order);
for (int q = 0; q < rule.GetNPoints(); ++q) {
const mfem::IntegrationPoint &point = rule.IntPoint(q);
transformation->SetIntPoint(&point);
compactification_element.CalcShape(point, compactification_shape);
const double coordinate =
element_compactification * compactification_shape;
const int shell = get_shell(coordinate);
mfem::Vector reference_field(3);
mfem::Vector physical_field(3);
mfem::Vector physical_position(3);
solution.gradPhi.GetVectorValue(element_id, point, reference_field);
mean_field::mapping::VolumeMappingContext context;
const mean_field::mapping::MappingStatus status =
f.domainMapperStateless->EvaluateVolume(mapping_data, *transformation,
point, workspace, context);
if (status != mean_field::mapping::MappingStatus::valid) {
++local_invalid_points;
continue;
}
physical_position = context.mapping.physical_position;
mean_field::mapping::MapHDivFluxToPhysical(
context.mapping, reference_field, physical_field);
const double radius = physical_position.Norml2();
if (!std::isfinite(radius) || radius <= 0.0) {
++local_invalid_points;
continue;
}
mfem::Vector radial_unit(physical_position);
radial_unit /= radius;
const double radial_field = physical_field * radial_unit;
mfem::Vector tangential_field(physical_field);
tangential_field.Add(-radial_field, radial_unit);
const double potential = solution.phi.GetValue(element_id, point);
const double scaled_potential =
-radius * potential / (mean_field::utils::G * mass);
const double scaled_radial_field =
radius * radius * radial_field / (mean_field::utils::G * mass);
const double scaled_tangential_field = radius * radius *
tangential_field.Norml2() /
(mean_field::utils::G * mass);
if (!std::isfinite(scaled_potential) ||
!std::isfinite(scaled_radial_field) ||
!std::isfinite(scaled_tangential_field)) {
++local_invalid_points;
continue;
}
const double weight = point.weight * transformation->Weight();
ShellAccumulator &accumulator = local[shell];
++accumulator.points;
accumulator.weight += weight;
accumulator.minimum_radius = std::min(accumulator.minimum_radius, radius);
accumulator.maximum_radius = std::max(accumulator.maximum_radius, radius);
accumulator.potential_error_squared +=
weight * (scaled_potential - 1.0) * (scaled_potential - 1.0);
accumulator.radial_error_squared +=
weight * (scaled_radial_field - 1.0) * (scaled_radial_field - 1.0);
accumulator.tangential_squared +=
weight * scaled_tangential_field * scaled_tangential_field;
}
}
MPI_Comm communicator = f.gravityFluxFes->GetComm();
ShellMeasurement measurement;
MPI_Allreduce(&local_invalid_points, &measurement.invalid_points, 1,
MPI_LONG_LONG, MPI_SUM, communicator);
for (int shell = 0; shell < shell_count; ++shell) {
long long points = 0;
MPI_Allreduce(&local[shell].points, &points, 1, MPI_LONG_LONG, MPI_SUM,
communicator);
const double local_sums[4]{
local[shell].weight, local[shell].potential_error_squared,
local[shell].radial_error_squared, local[shell].tangential_squared};
double sums[4]{};
MPI_Allreduce(local_sums, sums, 4, MPI_DOUBLE, MPI_SUM, communicator);
double minimum_radius = 0.0;
double maximum_radius = 0.0;
MPI_Allreduce(&local[shell].minimum_radius, &minimum_radius, 1, MPI_DOUBLE,
MPI_MIN, communicator);
MPI_Allreduce(&local[shell].maximum_radius, &maximum_radius, 1, MPI_DOUBLE,
MPI_MAX, communicator);
measurement.shells[shell] = {
.points = points,
.minimum_radius = minimum_radius,
.maximum_radius = maximum_radius,
.potential_rms_error = sums[0] > 0.0
? std::sqrt(sums[1] / sums[0])
: std::numeric_limits<double>::infinity(),
.radial_rms_error = sums[0] > 0.0
? std::sqrt(sums[2] / sums[0])
: std::numeric_limits<double>::infinity(),
.tangential_rms = sums[0] > 0.0
? std::sqrt(sums[3] / sums[0])
: std::numeric_limits<double>::infinity()};
}
return measurement;
}
GravitationalEnergies
compute_stellar_energies(mean_field::fem::FEM &f,
const mfem::GridFunction &density,
const mean_field::physics::GravitySolution &solution,
const mfem::GridFunction &displacement) {
mean_field::mapping::DomainMapper::Workspace workspace(
f.mesh->Dimension());
double local_binding = 0.0;
double local_virial = 0.0;
long long local_invalid_points = 0;
double local_minimum_determinant = std::numeric_limits<double>::infinity();
double local_maximum_determinant = -std::numeric_limits<double>::infinity();
const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute;
const int order = 2 * std::max(f.gravityPotentialFes->GetMaxElementOrder(),
f.gravityFluxFes->GetMaxElementOrder()) +
8;
std::array<long long, mapping_status_count> local_status_counts{};
for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) {
mfem::ElementTransformation *transformation =
f.mesh->GetElementTransformation(element_id);
if (transformation->Attribute == vacuum_attribute) {
continue;
}
const mfem::FiniteElement &displacement_element =
*f.displacementFes->GetFE(element_id);
const mfem::FiniteElement &compactification_element =
*f.compactificationFes->GetFE(element_id);
mfem::Array<int> displacement_dofs;
mfem::Array<int> compactification_dofs;
mfem::DofTransformation *displacement_transform =
f.displacementFes->GetElementVDofs(element_id, displacement_dofs);
mfem::DofTransformation *compactification_transform =
f.compactificationFes->GetElementDofs(element_id,
compactification_dofs);
mfem::Vector element_displacement;
mfem::Vector element_compactification;
displacement.GetSubVector(displacement_dofs, element_displacement);
f.compactificationCoordinate->GetSubVector(compactification_dofs,
element_compactification);
if (displacement_transform != nullptr) {
displacement_transform->InvTransformPrimal(element_displacement);
}
if (compactification_transform != nullptr) {
compactification_transform->InvTransformPrimal(element_compactification);
}
const mean_field::mapping::ElementDisplacementData displacement_data =
mean_field::mapping::ElementDisplacementDataFromElementVDofs(
displacement_element, element_displacement);
const mean_field::mapping::ElementCompactificationData
compactification_data(compactification_element,
element_compactification);
const mean_field::mapping::ElementMappingData mapping_data{
.displacement = displacement_data,
.compactification = compactification_data};
const mfem::IntegrationRule &rule =
mfem::IntRules.Get(transformation->GetGeometryType(), order);
for (int q = 0; q < rule.GetNPoints(); ++q) {
const mfem::IntegrationPoint &point = rule.IntPoint(q);
mean_field::mapping::VolumeMappingContext context;
const mean_field::mapping::MappingStatus status =
f.domainMapperStateless->EvaluateVolume(mapping_data, *transformation,
point, workspace, context);
const double mapping_determinant = context.mapping.mapping_determinant;
if (std::isfinite(mapping_determinant)) {
local_minimum_determinant =
std::min(local_minimum_determinant, mapping_determinant);
local_maximum_determinant =
std::max(local_maximum_determinant, mapping_determinant);
}
const int status_index = mapping_status_index(status);
MFEM_VERIFY(status_index >= 0 && status_index < mapping_status_count,
"Unexpected mapping status.");
++local_status_counts[status_index];
if (status != mean_field::mapping::MappingStatus::valid) {
++local_invalid_points;
continue;
}
if (status != mean_field::mapping::MappingStatus::valid) {
++local_invalid_points;
continue;
}
mfem::Vector reference_field(3);
mfem::Vector physical_field(3);
solution.gradPhi.GetVectorValue(element_id, point, reference_field);
mean_field::mapping::MapHDivFluxToPhysical(
context.mapping, reference_field, physical_field);
const double rho = density.GetValue(element_id, point);
const double phi = solution.phi.GetValue(element_id, point);
local_binding += 0.5 * rho * phi * context.quadrature.weight;
local_virial -= rho *
(context.mapping.physical_position * physical_field) *
context.quadrature.weight;
}
}
GravitationalEnergies energies;
MPI_Comm communicator = f.densityFes->GetComm();
MPI_Allreduce(&local_binding, &energies.binding, 1, MPI_DOUBLE, MPI_SUM,
communicator);
MPI_Allreduce(&local_virial, &energies.virial, 1, MPI_DOUBLE, MPI_SUM,
communicator);
MPI_Allreduce(&local_invalid_points, &energies.invalid_points, 1,
MPI_LONG_LONG, MPI_SUM, communicator);
MPI_Allreduce(local_status_counts.data(),
energies.mapping_status_counts.data(), mapping_status_count,
MPI_LONG_LONG, MPI_SUM, communicator);
MPI_Allreduce(&local_minimum_determinant,
&energies.minimum_mapping_determinant, 1, MPI_DOUBLE, MPI_MIN,
communicator);
MPI_Allreduce(&local_maximum_determinant,
&energies.maximum_mapping_determinant, 1, MPI_DOUBLE, MPI_MAX,
communicator);
return energies;
}
} // namespace
TEST_CASE("Gravity Field Monopole Accuracy And Projection Floor",
tags::gravity_analytic_accuracy) {
auto args = test_utils::setup_args();
args.p.rtol = 1.0e-13;
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.domainMapperStateless != nullptr);
REQUIRE(f.domainMapperStateless != nullptr);
REQUIRE(f.compactificationCoordinate != nullptr);
const double radius = mean_field::utils::RADIUS;
const double mass = mean_field::utils::MASS;
const double density_value =
mass / ((4.0 / 3.0) * M_PI * radius * radius * radius);
mfem::ParGridFunction displacement(f.displacementFes.get());
displacement = 0.0;
*f.displacement = 0.0;
mfem::GridFunction density(f.densityFes.get());
density = density_value;
zero_vacuum_density(f, density);
mean_field::analysis::conserve_mass(f, density, mass);
f.com = mean_field::analysis::get_com(f, density);
f.Q =
mean_field::physics::compute_quadrupole_moment_tensor(f, density, f.com);
const mean_field::physics::GravitySolution numerical_solution =
mean_field::physics::solve_gravity_field(f, args, density, displacement);
MonopolePotentialCoefficient potential_coefficient(
f, *f.domainMapperStateless, displacement, mass, radius);
mean_field::physics::GravitySolution projected_solution(f);
projected_solution.phi = 0.0;
projected_solution.gradPhi = 0.0;
projected_solution.phi.ProjectCoefficient(potential_coefficient);
const mfem::Vector projected_gradient_true =
project_monopole_gradient(f, displacement, mass, radius);
projected_solution.gradPhi.SetFromTrueDofs(projected_gradient_true);
const ShellMeasurement numerical =
measure_exterior_shells(f, numerical_solution, displacement, mass);
const ShellMeasurement projected =
measure_exterior_shells(f, projected_solution, displacement, mass);
const GravitationalEnergies energies =
compute_stellar_energies(f, density, numerical_solution, displacement);
mfem::Vector numerical_gradient;
mfem::Vector numerical_potential;
mfem::Vector projected_gradient;
mfem::Vector projected_potential;
numerical_solution.gradPhi.GetTrueDofs(numerical_gradient);
numerical_solution.phi.GetTrueDofs(numerical_potential);
projected_solution.gradPhi.GetTrueDofs(projected_gradient);
projected_solution.phi.GetTrueDofs(projected_potential);
MPI_Comm communicator = f.gravityFluxFes->GetComm();
const double gradient_projection_gap = mapped_hdiv_relative_error(
f, displacement, numerical_gradient, projected_gradient);
const double potential_projection_gap = global_relative_error(
numerical_potential, projected_potential, communicator);
double maximum_numerical_potential_error = 0.0;
double maximum_projected_potential_error = 0.0;
double maximum_numerical_radial_error = 0.0;
double maximum_projected_radial_error = 0.0;
double maximum_numerical_tangential = 0.0;
double maximum_projected_tangential = 0.0;
std::ostringstream report;
for (int shell = 0; shell < shell_count; ++shell) {
const ShellMetrics &numerical_shell = numerical.shells[shell];
const ShellMetrics &projected_shell = projected.shells[shell];
maximum_numerical_potential_error = std::max(
maximum_numerical_potential_error, numerical_shell.potential_rms_error);
maximum_projected_potential_error = std::max(
maximum_projected_potential_error, projected_shell.potential_rms_error);
maximum_numerical_radial_error = std::max(maximum_numerical_radial_error,
numerical_shell.radial_rms_error);
maximum_projected_radial_error = std::max(maximum_projected_radial_error,
projected_shell.radial_rms_error);
maximum_numerical_tangential =
std::max(maximum_numerical_tangential, numerical_shell.tangential_rms);
maximum_projected_tangential =
std::max(maximum_projected_tangential, projected_shell.tangential_rms);
report << "shell " << shell << " xi=[" << shell_boundaries[shell] << ", "
<< shell_boundaries[shell + 1] << ")\n"
<< " radius=[" << numerical_shell.minimum_radius << ", "
<< numerical_shell.maximum_radius << "]\n"
<< " potential error: solved="
<< numerical_shell.potential_rms_error
<< ", projection=" << projected_shell.potential_rms_error << '\n'
<< " radial error: solved=" << numerical_shell.radial_rms_error
<< ", projection=" << projected_shell.radial_rms_error << '\n'
<< " tangential amplitude: solved="
<< numerical_shell.tangential_rms
<< ", projection=" << projected_shell.tangential_rms << '\n';
}
const double analytic_energy =
-3.0 * mean_field::utils::G * mass * mass / (5.0 * radius);
const double binding_error =
std::abs(energies.binding - analytic_energy) / std::abs(analytic_energy);
const double virial_error =
std::abs(energies.virial - analytic_energy) / std::abs(analytic_energy);
const double consistency_error =
std::abs(energies.binding - energies.virial) / std::abs(energies.binding);
INFO(report.str());
INFO("Gradient solution/projection mapped H(div) gap = "
<< gradient_projection_gap);
INFO("Potential solution/projection DOF gap = " << potential_projection_gap);
INFO("Analytic energy = " << analytic_energy);
INFO("Computed binding energy = " << energies.binding);
INFO("Computed virial energy = " << energies.virial);
INFO("Relative binding error = " << binding_error);
INFO("Relative virial error = " << virial_error);
INFO("Relative virial consistency error = " << consistency_error);
REQUIRE(numerical.invalid_points == 0);
REQUIRE(projected.invalid_points == 0);
REQUIRE(energies.invalid_points == 0);
for (int shell = 0; shell < shell_count; ++shell) {
REQUIRE(numerical.shells[shell].points > 0);
REQUIRE(projected.shells[shell].points > 0);
}
CHECK(maximum_numerical_potential_error < 5.0e-2);
CHECK(maximum_projected_potential_error < 5.0e-2);
CHECK(maximum_numerical_radial_error < 5.0e-3);
CHECK(maximum_projected_radial_error < 5.0e-3);
CHECK(maximum_numerical_tangential < 5.0e-3);
CHECK(maximum_projected_tangential < 5.0e-3);
CHECK(gradient_projection_gap < 5.0e-3);
CHECK(potential_projection_gap < maximum_numerical_potential_error);
constexpr double virial_target = 1.0e-5;
CHECK(binding_error < virial_target);
CHECK(virial_error < virial_target);
CHECK(consistency_error < virial_target);
}
TEST_CASE(
"Gravity Field Virial Consistency Across Volume Preserving Deformation",
tags::gravity_consistency_accuracy) {
auto args = test_utils::setup_args();
args.p.rtol = 1.0e-13;
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.domainMapperStateless != nullptr);
REQUIRE(f.domainMapperStateless != nullptr);
const double radius = mean_field::utils::RADIUS;
const double mass = mean_field::utils::MASS;
const double central_density =
15.0 * mass / (8.0 * M_PI * radius * radius * radius);
auto density_function = [central_density,
radius](const mfem::Vector &position) {
const double normalized_radius_squared =
(position * position) / (radius * radius);
return central_density * std::max(0.0, 1.0 - normalized_radius_squared);
};
mfem::FunctionCoefficient density_coefficient(density_function);
mfem::GridFunction density(f.densityFes.get());
density.ProjectCoefficient(density_coefficient);
zero_vacuum_density(f, density);
mean_field::analysis::conserve_mass(f, density, mass);
constexpr std::array<double, 7> amplitudes{0.0, 0.02, 0.05, 0.1,
0.2, 0.5, 1.0};
std::array<double, amplitudes.size()> consistency_errors{};
std::array<double, amplitudes.size()> normalized_quadrupoles{};
std::array<double, amplitudes.size()> binding_energies{};
std::array<double, amplitudes.size()> virial_energies{};
std::ostringstream report;
for (std::size_t index = 0; index < amplitudes.size(); ++index) {
const double amplitude = amplitudes[index];
const double x_scale = 1.0 + 0.15 * amplitude;
const double y_scale = 1.0 - 0.05 * amplitude;
const double z_scale = 1.0 / (x_scale * y_scale);
REQUIRE_THAT(x_scale * y_scale * z_scale,
Catch::Matchers::WithinAbs(1.0, 1.0e-14));
auto displacement_function = [x_scale, y_scale,
z_scale](const mfem::Vector &position,
mfem::Vector &value) {
value.SetSize(3);
value(0) = (x_scale - 1.0) * position(0);
value(1) = (y_scale - 1.0) * position(1);
value(2) = (z_scale - 1.0) * position(2);
};
mfem::VectorFunctionCoefficient displacement_coefficient(
3, displacement_function);
mfem::ParGridFunction displacement(f.displacementFes.get());
displacement.ProjectCoefficient(displacement_coefficient);
*f.displacement = displacement;
f.com = mean_field::analysis::get_com(f, density);
f.Q = mean_field::physics::compute_quadrupole_moment_tensor(f, density,
f.com);
const mfem::FiniteElementSpace *nodal_space = f.mesh->GetNodalFESpace();
const mean_field::physics::GravitySolution solution =
mean_field::physics::solve_gravity_field(f, args, density,
displacement);
const GravitationalEnergies energies =
compute_stellar_energies(f, density, solution, displacement);
CAPTURE(amplitude, x_scale, y_scale, z_scale);
INFO("Mapping status valid = "
<< energies.mapping_status_counts[mapping_status_index(
mean_field::mapping::MappingStatus::valid)]);
INFO("Mapping status invalid_dimension = "
<< energies.mapping_status_counts[mapping_status_index(
mean_field::mapping::MappingStatus::invalid_dimension)]);
INFO("Mapping status non_finite_input = "
<< energies.mapping_status_counts[mapping_status_index(
mean_field::mapping::MappingStatus::non_finite_input)]);
INFO("Mapping status invalid_reference_radius = "
<< energies.mapping_status_counts[mapping_status_index(
mean_field::mapping::MappingStatus::invalid_reference_radius)]);
INFO("Mapping status at_compactified_infinity = "
<< energies.mapping_status_counts[mapping_status_index(
mean_field::mapping::MappingStatus::at_compactified_infinity)]);
INFO("Mapping status outside_reference_domain = "
<< energies.mapping_status_counts[mapping_status_index(
mean_field::mapping::MappingStatus::outside_reference_domain)]);
INFO("Mapping status non_finite_result = "
<< energies.mapping_status_counts[mapping_status_index(
mean_field::mapping::MappingStatus::non_finite_result)]);
INFO("Mapping status non_positive_determinant = "
<< energies.mapping_status_counts[mapping_status_index(
mean_field::mapping::MappingStatus::non_positive_determinant)]);
INFO("Total invalid mapping points = " << energies.invalid_points);
INFO("Mesh nodal order = "
<< (nodal_space != nullptr ? nodal_space->GetMaxElementOrder() : -1));
INFO("Displacement order = " << f.displacementFes->GetMaxElementOrder());
INFO("Minimum discrete mapping determinant = "
<< energies.minimum_mapping_determinant);
INFO("Maximum discrete mapping determinant = "
<< energies.maximum_mapping_determinant);
REQUIRE(energies.invalid_points == 0);
REQUIRE(std::isfinite(energies.binding));
REQUIRE(std::isfinite(energies.virial));
REQUIRE(energies.binding < 0.0);
REQUIRE(energies.virial < 0.0);
binding_energies[index] = energies.binding;
virial_energies[index] = energies.virial;
consistency_errors[index] = std::abs(energies.binding - energies.virial) /
std::abs(energies.binding);
normalized_quadrupoles[index] = f.Q.FNorm() / (mass * radius * radius);
report << "amplitude=" << amplitude << ", scales=(" << x_scale << ", "
<< y_scale << ", " << z_scale
<< "), normalized quadrupole=" << normalized_quadrupoles[index]
<< ", binding=" << binding_energies[index]
<< ", virial=" << virial_energies[index]
<< ", consistency error=" << consistency_errors[index] << '\n';
}
INFO(report.str());
for (std::size_t index = 1; index < amplitudes.size(); ++index) {
CHECK(normalized_quadrupoles[index] > normalized_quadrupoles[index - 1]);
}
CHECK(consistency_errors[0] < 1.0e-5);
for (std::size_t index = 1; index < amplitudes.size(); ++index) {
CHECK(consistency_errors[index] < 5.0e-5);
}
}