Files
MeanField/tests/physics/gravity.cpp

2467 lines
105 KiB
C++

#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <algorithm>
#include <array>
#include <cassert>
#include <cmath>
#include <limits>
#include <memory>
#include <boost/math/quadrature/gauss_kronrod.hpp>
#include <mfem.hpp>
#include <mpi.h>
import mean_field;
import test_helpers;
using namespace mean_field;
namespace {
// Concentration-16 rational profiles have a stable mixed-projection virial
// floor of approximately 1.53e-5 on the regression mesh. Increasing the
// diagnostic quadrature order changes each energy by only O(1e-13).
constexpr double rational_profile_virial_tolerance = 2.0e-5;
double global_vector_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_vector_dot(
const mfem::Vector &lhs,
const mfem::Vector &rhs,
MPI_Comm communicator
) {
const double local_dot = lhs * rhs;
double global_dot = 0.0;
MPI_Allreduce(&local_dot, &global_dot, 1, MPI_DOUBLE, MPI_SUM, communicator);
return global_dot;
}
double global_relative_vector_error(
const mfem::Vector &computed,
const mfem::Vector &reference,
MPI_Comm communicator
) {
mfem::Vector difference(computed);
difference -= reference;
return global_vector_norm(difference, communicator) /
std::max(global_vector_norm(reference, communicator), std::numeric_limits<double>::epsilon());
}
struct GravitationalEnergies {
double binding;
double virial;
};
struct HomogeneousEllipsoidAnalytic {
double coefficient_x;
double coefficient_y;
double coefficient_z;
double energy_kernel;
};
class HomogeneousEllipsoidHDivCoefficient : public mfem::VectorCoefficient {
public:
HomogeneousEllipsoidHDivCoefficient(
const mapping::DomainMapper &domain_mapper,
const mfem::GridFunction &displacement,
const mfem::GridFunction &compactification_coordinate,
const double density,
const HomogeneousEllipsoidAnalytic &analytic
)
: VectorCoefficient(3),
mapping_evaluator(
domain_mapper,
displacement,
compactification_coordinate
),
density(density),
coefficient_x(analytic.coefficient_x),
coefficient_y(analytic.coefficient_y),
coefficient_z(analytic.coefficient_z) {
}
void Eval(
mfem::Vector &value,
mfem::ElementTransformation &transformation,
const mfem::IntegrationPoint &integration_point
) override {
transformation.SetIntPoint(&integration_point);
mfem::Vector field_physical(3);
mapping::MappingPointContext context;
MFEM_VERIFY(
mapping_evaluator.EvaluatePoint(transformation, integration_point, context) ==
mapping::MappingStatus::valid,
"Ellipsoid coefficient encountered an invalid mapping."
);
const mfem::Vector &x_physical = context.physical_position;
field_physical(0) = 2.0 * M_PI * utils::G * density * coefficient_x * x_physical(0);
field_physical(1) = 2.0 * M_PI * utils::G * density * coefficient_y * x_physical(1);
field_physical(2) = 2.0 * M_PI * utils::G * density * coefficient_z * x_physical(2);
mapping::MapPhysicalFluxToHDivReference(context, field_physical, value);
}
private:
mapping::GridFunctionMappingEvaluator mapping_evaluator;
double density;
double coefficient_x;
double coefficient_y;
double coefficient_z;
};
template <typename GravitySolutionType>
GravitationalEnergies compute_gravitational_energies(
fem::FEM &f,
const mfem::GridFunction &rho,
const GravitySolutionType &gravity_solution,
const int quadrature_order
) {
const int dim = f.mesh->Dimension();
double local_bind_integral = 0.0;
double local_virial_integral = 0.0;
mfem::Vector x_physical(dim);
mfem::Vector grad_phi_element(dim);
mfem::Vector grad_phi_physical(dim);
using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema;
mapping::GridFunctionMappingEvaluator mapping_evaluator(
*f.domainMapperStateless, *f.displacement, *f.compactificationCoordinate
);
for (int elem_id = 0; elem_id < f.mesh->GetNE(); ++elem_id) {
if (!DomainSchema::template attribute_belongs_to<utils::domain::Stellar>(f.mesh->GetAttribute(elem_id))) {
continue;
}
mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(elem_id);
const mfem::IntegrationRule &integration_rule =
mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order);
for (int q = 0; q < integration_rule.GetNPoints(); ++q) {
const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q);
transformation->SetIntPoint(&integration_point);
mapping::VolumeMappingContext mapping_context;
MFEM_VERIFY(
mapping_evaluator.EvaluateVolume(*transformation, integration_point, mapping_context) ==
mapping::MappingStatus::valid,
"Gravity-energy integration encountered an invalid mapping."
);
const double weight = mapping_context.quadrature.weight;
x_physical = mapping_context.mapping.physical_position;
gravity_solution.gradPhi.GetVectorValue(elem_id, integration_point, grad_phi_element);
mapping::MapHDivFluxToPhysical(mapping_context.mapping, grad_phi_element, grad_phi_physical);
const double rho_value = rho.GetValue(elem_id, integration_point);
const double phi_value = gravity_solution.phi.GetValue(elem_id, integration_point);
double radius_dot_gradient = 0.0;
for (int d = 0; d < dim; ++d) {
radius_dot_gradient += (x_physical(d) - f.com(d)) * grad_phi_physical(d);
}
local_bind_integral += rho_value * phi_value * weight;
local_virial_integral += rho_value * radius_dot_gradient * weight;
}
}
const double local_w_bind = 0.5 * local_bind_integral;
const double local_w_vir = -local_virial_integral;
double global_w_bind = 0.0;
double global_w_vir = 0.0;
MPI_Comm communicator = f.densityFes->GetComm();
MPI_Allreduce(&local_w_bind, &global_w_bind, 1, MPI_DOUBLE, MPI_SUM, communicator);
MPI_Allreduce(&local_w_vir, &global_w_vir, 1, MPI_DOUBLE, MPI_SUM, communicator);
return {.binding = global_w_bind, .virial = global_w_vir};
}
void zero_vacuum_density(
const fem::FEM &f,
mfem::GridFunction &rho
) {
using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema;
const field::FieldDofMap densityMap = field::make_field_dof_map<field::Density, DomainSchema>(*f.densityFes);
mfem::Vector densityTrue;
rho.GetTrueDofs(densityTrue);
const mfem::Vector supportedDensity = densityMap.gather(densityTrue);
densityMap.scatter(supportedDensity, densityTrue);
rho.SetFromTrueDofs(densityTrue);
}
int get_gravity_quadrature_order(const fem::FEM &f) {
return 2 * std::max(f.gravityPotentialFes->GetMaxElementOrder(), f.gravityFluxFes->GetMaxElementOrder()) + 8;
}
double compute_ellipsoid_coefficient(
const double normalized_axis_x,
const double normalized_axis_y,
const double normalized_axis_z,
const double target_axis_squared
) {
auto integrand = [=](const double t) {
if (t <= 0.0 || t >= 1.0) {
return 0.0;
}
const double one_minus_t = 1.0 - t;
const double s = t / one_minus_t;
const double s_squared = s * s;
const double ds_squared_dt = 2.0 * s / (one_minus_t * one_minus_t);
const double delta = std::sqrt(
(normalized_axis_x * normalized_axis_x + s_squared) *
(normalized_axis_y * normalized_axis_y + s_squared) *
(normalized_axis_z * normalized_axis_z + s_squared)
);
return normalized_axis_x * normalized_axis_y * normalized_axis_z * ds_squared_dt /
((target_axis_squared + s_squared) * delta);
};
double integration_error = 0.0;
return boost::math::quadrature::gauss_kronrod<double, 61>::integrate(
integrand, 0.0, 1.0, 15, 1.0e-13, &integration_error
);
}
double compute_ellipsoid_energy_kernel(
const double normalized_axis_x,
const double normalized_axis_y,
const double normalized_axis_z,
const double length_scale
) {
auto integrand = [=](const double t) {
if (t <= 0.0) {
return 0.0;
}
if (t >= 1.0) {
return 2.0;
}
const double one_minus_t = 1.0 - t;
const double s = t / one_minus_t;
const double s_squared = s * s;
const double ds_squared_dt = 2.0 * s / (one_minus_t * one_minus_t);
const double delta = std::sqrt(
(normalized_axis_x * normalized_axis_x + s_squared) *
(normalized_axis_y * normalized_axis_y + s_squared) *
(normalized_axis_z * normalized_axis_z + s_squared)
);
return ds_squared_dt / delta;
};
double integration_error = 0.0;
const double dimensionless_integral = boost::math::quadrature::gauss_kronrod<double, 61>::integrate(
integrand, 0.0, 1.0, 15, 1.0e-13, &integration_error
);
return dimensionless_integral / length_scale;
}
HomogeneousEllipsoidAnalytic compute_homogeneous_ellipsoid_analytic(
const double semi_axis_x,
const double semi_axis_y,
const double semi_axis_z
) {
const double length_scale = std::cbrt(semi_axis_x * semi_axis_y * semi_axis_z);
const double normalized_axis_x = semi_axis_x / length_scale;
const double normalized_axis_y = semi_axis_y / length_scale;
const double normalized_axis_z = semi_axis_z / length_scale;
const double coefficient_x = compute_ellipsoid_coefficient(
normalized_axis_x, normalized_axis_y, normalized_axis_z, normalized_axis_x * normalized_axis_x
);
const double coefficient_y = compute_ellipsoid_coefficient(
normalized_axis_x, normalized_axis_y, normalized_axis_z, normalized_axis_y * normalized_axis_y
);
const double coefficient_z = compute_ellipsoid_coefficient(
normalized_axis_x, normalized_axis_y, normalized_axis_z, normalized_axis_z * normalized_axis_z
);
const double energy_kernel =
compute_ellipsoid_energy_kernel(normalized_axis_x, normalized_axis_y, normalized_axis_z, length_scale);
return {
.coefficient_x = coefficient_x,
.coefficient_y = coefficient_y,
.coefficient_z = coefficient_z,
.energy_kernel = energy_kernel
};
}
struct ExteriorMonopoleShellMetrics {
long long quadrature_points{0};
double minimum_radius{std::numeric_limits<double>::infinity()};
double maximum_radius{0.0};
double potential_rms_error{0.0};
double radial_field_rms_error{0.0};
double tangential_field_rms{0.0};
};
struct ExteriorMonopoleShellAccumulator {
long long quadrature_points{0};
double minimum_radius{std::numeric_limits<double>::infinity()};
double maximum_radius{0.0};
double reference_weight{0.0};
double potential_error_squared{0.0};
double radial_field_error_squared{0.0};
double tangential_field_squared{0.0};
};
constexpr std::array<double, 6> exterior_shell_boundaries{0.0, 0.25, 0.50, 0.75, 0.90, 1.0};
int get_exterior_shell(const double compactification_coordinate) {
REQUIRE(std::isfinite(compactification_coordinate));
REQUIRE(compactification_coordinate >= -1.0e-12);
REQUIRE(compactification_coordinate <= 1.0 + 1.0e-12);
const double coordinate = std::clamp(compactification_coordinate, 0.0, std::nextafter(1.0, 0.0));
for (int shell = 0; shell < static_cast<int>(exterior_shell_boundaries.size()) - 1; ++shell) {
if (coordinate < exterior_shell_boundaries[shell + 1]) {
return shell;
}
}
return static_cast<int>(exterior_shell_boundaries.size()) - 2;
}
std::array<
ExteriorMonopoleShellMetrics,
5>
measure_exterior_monopole_shells(
fem::FEM &f,
const physics::GravitySolution &solution,
const mfem::GridFunction &displacement,
const double mass
) {
REQUIRE(f.mesh != nullptr);
REQUIRE(f.gravityFluxFes != nullptr);
REQUIRE(f.displacementFes != nullptr);
REQUIRE(f.compactificationFes != nullptr);
REQUIRE(f.compactificationCoordinate != nullptr);
REQUIRE(f.domainMapperStateless != nullptr);
REQUIRE(f.domainMapperStateless != nullptr);
constexpr int shell_count = static_cast<int>(exterior_shell_boundaries.size()) - 1;
std::array<ExteriorMonopoleShellAccumulator, shell_count> local_shells{};
mapping::DomainMapper::Workspace workspace(f.mesh->Dimension());
const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute;
const int quadrature_order = get_gravity_quadrature_order(f);
for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) {
mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(element_id);
REQUIRE(transformation != nullptr);
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_dof_transformation =
f.displacementFes->GetElementVDofs(element_id, displacement_dofs);
mfem::DofTransformation *compactification_dof_transformation =
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_dof_transformation != nullptr) {
displacement_dof_transformation->InvTransformPrimal(element_displacement);
}
if (compactification_dof_transformation != nullptr) {
compactification_dof_transformation->InvTransformPrimal(element_compactification);
}
const mapping::ElementDisplacementData displacement_data(
displacement_element, element_displacement, f.displacementFes->GetOrdering()
);
const mapping::ElementCompactificationData compactification_data(
compactification_element, element_compactification
);
const mapping::ElementMappingData mapping_data{
.displacement = displacement_data, .compactification = compactification_data
};
mfem::Vector compactification_shape(compactification_element.GetDof());
const mfem::IntegrationRule &integration_rule =
mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order);
for (int q = 0; q < integration_rule.GetNPoints(); ++q) {
const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q);
transformation->SetIntPoint(&integration_point);
compactification_element.CalcShape(integration_point, compactification_shape);
const double compactification_coordinate = element_compactification * compactification_shape;
const int shell = get_exterior_shell(compactification_coordinate);
mfem::Vector reference_field(3);
mfem::Vector physical_field(3);
mfem::Vector physical_position(3);
solution.gradPhi.GetVectorValue(element_id, integration_point, reference_field);
mapping::VolumeMappingContext mapping_context;
const mapping::MappingStatus status = f.domainMapperStateless->EvaluateVolume(
mapping_data, *transformation, integration_point, workspace, mapping_context
);
CAPTURE(element_id, q, compactification_coordinate, static_cast<int>(status));
REQUIRE(status == mean_field::mapping::MappingStatus::valid);
physical_position = mapping_context.mapping.physical_position;
mean_field::mapping::MapHDivFluxToPhysical(mapping_context.mapping, reference_field, physical_field);
const double radius = physical_position.Norml2();
CAPTURE(element_id, q, shell, compactification_coordinate, radius);
REQUIRE(std::isfinite(radius));
REQUIRE(radius > 0.0);
mfem::Vector radial_unit_vector(physical_position);
radial_unit_vector /= radius;
const double numerical_radial_field = physical_field * radial_unit_vector;
mfem::Vector tangential_field(physical_field);
tangential_field.Add(-numerical_radial_field, radial_unit_vector);
const double numerical_potential = solution.phi.GetValue(element_id, integration_point);
/*
* For an exterior monopole:
*
* phi = -GM/r
* grad(phi) = GM r_hat/r^2
*
* These scaled quantities should therefore be one, one, and
* zero respectively. They remain well-conditioned as r -> inf.
*/
const double scaled_potential = -radius * numerical_potential / (utils::G * mass);
const double scaled_radial_field = radius * radius * numerical_radial_field / (utils::G * mass);
const double scaled_tangential_field = radius * radius * tangential_field.Norml2() / (utils::G * mass);
REQUIRE(std::isfinite(scaled_potential));
REQUIRE(std::isfinite(scaled_radial_field));
REQUIRE(std::isfinite(scaled_tangential_field));
/*
* Use the finite reference-domain measure for averaging. A
* physical L2 norm of phi over an infinite three-dimensional
* exterior domain is not finite.
*/
const double reference_weight = integration_point.weight * transformation->Weight();
ExteriorMonopoleShellAccumulator &accumulator = local_shells[shell];
++accumulator.quadrature_points;
accumulator.minimum_radius = std::min(accumulator.minimum_radius, radius);
accumulator.maximum_radius = std::max(accumulator.maximum_radius, radius);
accumulator.reference_weight += reference_weight;
accumulator.potential_error_squared += reference_weight * std::pow(scaled_potential - 1.0, 2);
accumulator.radial_field_error_squared += reference_weight * std::pow(scaled_radial_field - 1.0, 2);
accumulator.tangential_field_squared +=
reference_weight * scaled_tangential_field * scaled_tangential_field;
}
}
MPI_Comm communicator = f.gravityFluxFes->GetComm();
std::array<ExteriorMonopoleShellMetrics, shell_count> metrics{};
for (int shell = 0; shell < shell_count; ++shell) {
long long global_points = 0;
MPI_Allreduce(
&local_shells[shell].quadrature_points, &global_points, 1, MPI_LONG_LONG, MPI_SUM, communicator
);
double local_sums[4]{
local_shells[shell].reference_weight, local_shells[shell].potential_error_squared,
local_shells[shell].radial_field_error_squared, local_shells[shell].tangential_field_squared
};
double global_sums[4]{};
MPI_Allreduce(local_sums, global_sums, 4, MPI_DOUBLE, MPI_SUM, communicator);
double global_minimum_radius = 0.0;
double global_maximum_radius = 0.0;
MPI_Allreduce(
&local_shells[shell].minimum_radius, &global_minimum_radius, 1, MPI_DOUBLE, MPI_MIN, communicator
);
MPI_Allreduce(
&local_shells[shell].maximum_radius, &global_maximum_radius, 1, MPI_DOUBLE, MPI_MAX, communicator
);
REQUIRE(global_points > 0);
REQUIRE(global_sums[0] > 0.0);
metrics[shell] = {
.quadrature_points = global_points,
.minimum_radius = global_minimum_radius,
.maximum_radius = global_maximum_radius,
.potential_rms_error = std::sqrt(global_sums[1] / global_sums[0]),
.radial_field_rms_error = std::sqrt(global_sums[2] / global_sums[0]),
.tangential_field_rms = std::sqrt(global_sums[3] / global_sums[0])
};
}
return metrics;
}
class StatelessProjectionGeometry {
public:
StatelessProjectionGeometry(
const fem::FEM &f,
const mapping::DomainMapper &domain_mapper,
const mfem::GridFunction &displacement
)
: m_fem(f),
m_domain_mapper(domain_mapper),
m_displacement(displacement),
m_workspace(f.mesh->Dimension()) {
}
mapping::MappingStatus Evaluate(
mfem::ElementTransformation &transformation,
const mfem::IntegrationPoint &integration_point,
mapping::MappingPointContext &context,
const bool permit_infinity_limit
) {
m_last_evaluation_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_dof_transformation =
m_fem.displacementFes->GetElementVDofs(element_id, displacement_dofs);
mfem::DofTransformation *compactification_dof_transformation =
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_dof_transformation != nullptr) {
displacement_dof_transformation->InvTransformPrimal(element_displacement);
}
if (compactification_dof_transformation != nullptr) {
compactification_dof_transformation->InvTransformPrimal(element_compactification);
}
const mapping::ElementDisplacementData displacement_data(
displacement_element, element_displacement, m_fem.displacementFes->GetOrdering()
);
const mapping::ElementCompactificationData compactification_data(
compactification_element, element_compactification
);
mfem::Vector requested_compactification_shape(compactification_element.GetDof());
compactification_element.CalcShape(integration_point, requested_compactification_shape);
const double requested_compactification_coordinate =
element_compactification * requested_compactification_shape;
constexpr double infinity_candidate_tolerance = 1.0e-8;
const bool requested_infinity_limit =
std::isfinite(requested_compactification_coordinate) &&
requested_compactification_coordinate >= 1.0 - infinity_candidate_tolerance;
const mapping::ElementMappingData mapping_data{
.displacement = displacement_data, .compactification = compactification_data
};
mapping::MappingStatus status =
m_domain_mapper.EvaluatePoint(mapping_data, transformation, integration_point, m_workspace, context);
if (status == mapping::MappingStatus::valid) {
transformation.SetIntPoint(&integration_point);
return status;
}
if (!permit_infinity_limit || !m_domain_mapper.IsCompactifiedElement(transformation)) {
transformation.SetIntPoint(&integration_point);
return status;
}
const bool retryable_boundary_status =
status == mapping::MappingStatus::at_compactified_infinity ||
status == mapping::MappingStatus::outside_reference_domain ||
status == mapping::MappingStatus::non_finite_result ||
(requested_infinity_limit && status == mapping::MappingStatus::non_positive_determinant);
if (!retryable_boundary_status) {
transformation.SetIntPoint(&integration_point);
return status;
}
const mfem::IntegrationPoint &element_center = mfem::Geometries.GetCenter(transformation.GetGeometryType());
/*
* Use the nearest admissible point. Starting extremely close to
* the requested point preserves the limiting RT trace, while the
* larger fallbacks accommodate the mapper's infinity guard.
*/
constexpr std::array<double, 9> 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};
for (const double inward_fraction : inward_fractions) {
mfem::IntegrationPoint inward_point;
inward_point.x = (1.0 - inward_fraction) * integration_point.x + inward_fraction * element_center.x;
inward_point.y = (1.0 - inward_fraction) * integration_point.y + inward_fraction * element_center.y;
inward_point.z = (1.0 - inward_fraction) * integration_point.z + inward_fraction * element_center.z;
inward_point.weight = integration_point.weight;
status =
m_domain_mapper.EvaluatePoint(mapping_data, transformation, inward_point, m_workspace, context);
if (status == mapping::MappingStatus::valid) {
m_last_evaluation_used_infinity_limit = true;
transformation.SetIntPoint(&integration_point);
return status;
}
const bool still_retryable =
status == mapping::MappingStatus::at_compactified_infinity ||
status == mapping::MappingStatus::outside_reference_domain ||
status == mapping::MappingStatus::non_finite_result ||
(requested_infinity_limit && status == mapping::MappingStatus::non_positive_determinant);
if (!still_retryable) {
break;
}
}
transformation.SetIntPoint(&integration_point);
return status;
}
[[nodiscard]]
bool LastEvaluationUsedInfinityLimit() const noexcept {
return m_last_evaluation_used_infinity_limit;
}
private:
const fem::FEM &m_fem;
const mapping::DomainMapper &m_domain_mapper;
const mfem::GridFunction &m_displacement;
mapping::DomainMapper::Workspace m_workspace;
bool m_last_evaluation_used_infinity_limit{false};
};
class StatelessMonopolePotentialCoefficient final : public mfem::Coefficient {
public:
StatelessMonopolePotentialCoefficient(
const fem::FEM &f,
const mapping::DomainMapper &domain_mapper,
const mfem::GridFunction &displacement,
const double mass,
const double stellar_radius
)
: m_geometry(
f,
domain_mapper,
displacement
),
m_vacuum_attribute(field_dof_test_utils::vacuum_material_attribute),
m_mass(mass),
m_stellar_radius(stellar_radius) {
}
double Eval(
mfem::ElementTransformation &transformation,
const mfem::IntegrationPoint &integration_point
) override {
mapping::MappingPointContext context;
const mapping::MappingStatus status = m_geometry.Evaluate(transformation, integration_point, context, true);
MFEM_VERIFY(
status == mean_field::mapping::MappingStatus::valid,
"Stateless monopole-potential projection failed."
<< "\nMapping status = " << static_cast<int>(status)
<< "\nElement ID = " << transformation.ElementNo
<< "\nElement attribute = " << transformation.Attribute << "\nIntegration point = <"
<< integration_point.x << ", " << integration_point.y << ", " << integration_point.z << ">"
);
if (m_geometry.LastEvaluationUsedInfinityLimit()) {
return 0.0;
}
const double radius = context.physical_position.Norml2();
MFEM_VERIFY(
std::isfinite(radius) && radius > 0.0, "Monopole projection encountered an invalid physical radius."
);
if (transformation.Attribute == m_vacuum_attribute) {
return -utils::G * m_mass / radius;
}
return -utils::G * m_mass / (2.0 * m_stellar_radius * m_stellar_radius * m_stellar_radius) *
(3.0 * m_stellar_radius * m_stellar_radius - radius * radius);
}
private:
StatelessProjectionGeometry m_geometry;
int m_vacuum_attribute;
double m_mass;
double m_stellar_radius;
};
class StatelessMonopoleHDivCoefficient final : public mfem::VectorCoefficient {
public:
StatelessMonopoleHDivCoefficient(
const fem::FEM &f,
const mapping::DomainMapper &domain_mapper,
const mfem::GridFunction &displacement,
const double mass,
const double stellar_radius
)
: VectorCoefficient(f.mesh->Dimension()),
m_geometry(
f,
domain_mapper,
displacement
),
m_vacuum_attribute(field_dof_test_utils::vacuum_material_attribute),
m_mass(mass),
m_stellar_radius(stellar_radius) {
}
void Eval(
mfem::Vector &value,
mfem::ElementTransformation &transformation,
const mfem::IntegrationPoint &integration_point
) override {
mapping::MappingPointContext context;
const mapping::MappingStatus status = m_geometry.Evaluate(transformation, integration_point, context, true);
MFEM_VERIFY(
status == mean_field::mapping::MappingStatus::valid,
"Stateless monopole H(div) projection failed."
<< "\nMapping status = " << static_cast<int>(status)
<< "\nElement ID = " << transformation.ElementNo
<< "\nElement attribute = " << transformation.Attribute << "\nIntegration point = <"
<< integration_point.x << ", " << integration_point.y << ", " << integration_point.z << ">"
<< "\nInfinity-limit evaluation attempted = " << m_geometry.LastEvaluationUsedInfinityLimit()
);
const mfem::Vector &displaced_position = context.displaced_position;
const double computational_radius = displaced_position.Norml2();
MFEM_VERIFY(
std::isfinite(computational_radius) && computational_radius > 0.0,
"Monopole H(div) projection encountered an invalid displaced "
"computational radius."
);
const double displacement_determinant = context.displacement_jacobian.Det();
MFEM_VERIFY(
std::isfinite(displacement_determinant) && displacement_determinant > 0.0,
"Monopole H(div) projection encountered an invalid "
"displacement "
"Jacobian determinant."
);
mfem::DenseMatrix inverse_displacement_jacobian;
inverse_displacement_jacobian.SetSize(
context.displacement_jacobian.Height(), context.displacement_jacobian.Width()
);
mfem::CalcInverse(context.displacement_jacobian, inverse_displacement_jacobian);
/*
* Pull the radial field back only through the regular displacement
* map.
*
* In the compactified vacuum, the Kelvin scale and its radial
* derivative cancel exactly from the three-dimensional H(div) Piola
* pullback of the inverse-square monopole field:
*
* det(J) J^{-1} (GM x / |x|^3)
* = GM det(A) A^{-1} y / |y|^3.
*
* This is also the finite reference-space limit at compactified
* infinity.
*/
inverse_displacement_jacobian.Mult(displaced_position, value);
double radial_denominator = 0.0;
if (transformation.Attribute == m_vacuum_attribute) {
radial_denominator = computational_radius * computational_radius * computational_radius;
} else {
radial_denominator = m_stellar_radius * m_stellar_radius * m_stellar_radius;
}
value *= mean_field::utils::G * m_mass * displacement_determinant / radial_denominator;
for (int component = 0; component < value.Size(); ++component) {
MFEM_VERIFY(
std::isfinite(value(component)), "Monopole H(div) projection produced a non-finite "
"reference flux."
);
}
}
private:
StatelessProjectionGeometry m_geometry;
int m_vacuum_attribute;
double m_mass;
double m_stellar_radius;
};
} // namespace
TEST_CASE(
"Uniform Potential Matches Analytic",
tags::gravity_analytic
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
*f.displacement = 0.0;
mfem::ParGridFunction displacement(f.displacementFes.get());
displacement = 0.0;
const double radius = utils::RADIUS;
const double mass = utils::MASS;
const double analytic_volume = (4.0 / 3.0) * M_PI * std::pow(radius, 3.0);
const double density = mass / analytic_volume;
mfem::GridFunction rho_uniform(f.densityFes.get());
rho_uniform = density;
zero_vacuum_density(f, rho_uniform);
analysis::conserve_mass(f, rho_uniform, mass);
f.com = analysis::get_com(f, rho_uniform);
f.Q = physics::compute_quadrupole_moment_tensor(f, rho_uniform, f.com);
const auto gravity_solution = physics::solve_gravity_field(f, args, rho_uniform, displacement);
constexpr double potential_tolerance = utils::APPROX_MAX_ACCEPTABLE_POTENTIAL_ERROR_SI_BURNING;
double local_max_abs_error = 0.0;
double local_max_rel_error = 0.0;
mapping::GridFunctionMappingEvaluator mapping_evaluator(
*f.domainMapperStateless, *f.displacement, *f.compactificationCoordinate
);
const int num_elements_to_test = std::min(30, f.mesh->GetNE());
for (int elem_id = 0; elem_id < num_elements_to_test; ++elem_id) {
mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(elem_id);
const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get(transformation->GetGeometryType(), 2);
const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(0);
transformation->SetIntPoint(&integration_point);
mfem::Vector x_physical;
mapping_evaluator.GetPhysicalPoint(*transformation, integration_point, x_physical);
const double radial_coordinate = x_physical.Norml2();
if (radial_coordinate < 1.0e-9) {
continue;
}
const double phi_analytic = -(utils::G * mass / (2.0 * std::pow(radius, 3.0))) *
(3.0 * radius * radius - radial_coordinate * radial_coordinate);
const double phi_fem = gravity_solution.phi.GetValue(elem_id, integration_point);
const double absolute_error = std::abs(phi_fem - phi_analytic);
const double relative_error = absolute_error / std::abs(phi_analytic);
local_max_abs_error = std::max(local_max_abs_error, absolute_error);
local_max_rel_error = std::max(local_max_rel_error, relative_error);
CHECK_THAT(relative_error, Catch::Matchers::WithinAbs(0.0, 0.1 * potential_tolerance));
}
double global_max_abs_error = 0.0;
double global_max_rel_error = 0.0;
MPI_Comm communicator = f.densityFes->GetComm();
MPI_Allreduce(&local_max_abs_error, &global_max_abs_error, 1, MPI_DOUBLE, MPI_MAX, communicator);
MPI_Allreduce(&local_max_rel_error, &global_max_rel_error, 1, MPI_DOUBLE, MPI_MAX, communicator);
const int quadrature_order = get_gravity_quadrature_order(f);
const GravitationalEnergies energies =
compute_gravitational_energies(f, rho_uniform, gravity_solution, quadrature_order);
const double analytic_binding_energy = -(3.0 / 5.0) * utils::G * mass * mass / radius;
const double relative_binding_error =
std::abs(energies.binding - analytic_binding_energy) / std::abs(analytic_binding_energy);
const double relative_virial_error =
std::abs(energies.virial - analytic_binding_energy) / std::abs(analytic_binding_energy);
const double relative_consistency_error = std::abs(energies.binding - energies.virial) / std::abs(energies.binding);
INFO("Analytic binding energy = " << analytic_binding_energy);
INFO("Computed binding energy = " << energies.binding);
INFO("Computed virial energy = " << energies.virial);
INFO("Relative virial consistency error = " << relative_consistency_error);
constexpr double energy_tolerance = 1.0e-5;
constexpr double consistency_tolerance = 1.0e-6;
CHECK_THAT(global_max_rel_error, Catch::Matchers::WithinAbs(0.0, 0.1 * potential_tolerance));
CHECK_THAT(global_max_abs_error, Catch::Matchers::WithinAbs(0.0, 0.1 * potential_tolerance));
CHECK_THAT(relative_binding_error, Catch::Matchers::WithinAbs(0.0, energy_tolerance));
CHECK_THAT(relative_virial_error, Catch::Matchers::WithinAbs(0.0, energy_tolerance));
CHECK_THAT(relative_consistency_error, Catch::Matchers::WithinAbs(0.0, consistency_tolerance));
}
TEST_CASE(
"Parabolic Density Virial Self-Consistency",
tags::gravity_analytic
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
*f.displacement = 0.0;
mfem::ParGridFunction displacement(f.displacementFes.get());
displacement = 0.0;
const double radius = utils::RADIUS;
const double mass = utils::MASS;
const double central_density = (15.0 * mass) / (8.0 * M_PI * std::pow(radius, 3.0));
auto parabolic_rho = [central_density, radius](const mfem::Vector &x) {
const double radial_coordinate = x.Norml2();
return central_density * (1.0 - radial_coordinate * radial_coordinate / (radius * radius));
};
std::unique_ptr<mfem::Coefficient> rho_coeff;
if (f.has_mapping()) {
rho_coeff = std::make_unique<mapping::PhysicalPositionFunctionCoefficient>(
*f.domainMapperStateless, *f.displacement, *f.compactificationCoordinate, parabolic_rho
);
} else {
rho_coeff = std::make_unique<mfem::FunctionCoefficient>(parabolic_rho);
}
mfem::GridFunction rho_grid(f.densityFes.get());
rho_grid.ProjectCoefficient(*rho_coeff);
zero_vacuum_density(f, rho_grid);
analysis::conserve_mass(f, rho_grid, mass);
f.com = analysis::get_com(f, rho_grid);
f.Q = physics::compute_quadrupole_moment_tensor(f, rho_grid, f.com);
const auto gravity_solution = physics::solve_gravity_field(f, args, rho_grid, displacement);
const int quadrature_order = get_gravity_quadrature_order(f);
const GravitationalEnergies energies =
compute_gravitational_energies(f, rho_grid, gravity_solution, quadrature_order);
const double analytic_binding_energy = -(5.0 / 7.0) * utils::G * mass * mass / radius;
const double relative_binding_error =
std::abs(energies.binding - analytic_binding_energy) / std::abs(analytic_binding_energy);
const double relative_virial_error =
std::abs(energies.virial - analytic_binding_energy) / std::abs(analytic_binding_energy);
const double relative_consistency_error = std::abs(energies.binding - energies.virial) / std::abs(energies.binding);
INFO("Analytic binding energy = " << analytic_binding_energy);
INFO("Computed binding energy = " << energies.binding);
INFO("Computed virial energy = " << energies.virial);
INFO("Relative virial consistency error = " << relative_consistency_error);
constexpr double analytic_tolerance = 1.0e-5;
constexpr double consistency_tolerance = 1.0e-6;
CHECK_THAT(relative_binding_error, Catch::Matchers::WithinAbs(0.0, analytic_tolerance));
CHECK_THAT(relative_virial_error, Catch::Matchers::WithinAbs(0.0, analytic_tolerance));
CHECK_THAT(relative_consistency_error, Catch::Matchers::WithinAbs(0.0, consistency_tolerance));
}
TEST_CASE(
"Rational Density Virial Self-Consistency",
tags::gravity_consistency
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
*f.displacement = 0.0;
mfem::ParGridFunction displacement(f.displacementFes.get());
displacement = 0.0;
const double radius = utils::RADIUS;
const double mass = utils::MASS;
// Larger values are more centrally concentrated and generally harder for a
// polynomial to represent. A regression would be considered if this test
// does not pass for concentrations <= 16.0.
constexpr double concentration = 16.0;
const double density_scale = mass / std::pow(radius, 3.0);
auto rational_rho = [radius, density_scale](const mfem::Vector &x) {
const double normalized_radius_squared = (x * x) / (radius * radius);
if (normalized_radius_squared >= 1.0) {
return 0.0;
}
const double denominator = 1.0 + concentration * normalized_radius_squared;
return density_scale * (1.0 - normalized_radius_squared) / (denominator * denominator);
};
std::unique_ptr<mfem::Coefficient> rho_coeff;
if (f.has_mapping()) {
rho_coeff = std::make_unique<mapping::PhysicalPositionFunctionCoefficient>(
*f.domainMapperStateless, *f.displacement, *f.compactificationCoordinate, rational_rho
);
} else {
rho_coeff = std::make_unique<mfem::FunctionCoefficient>(rational_rho);
}
mfem::GridFunction rho_grid(f.densityFes.get());
rho_grid.ProjectCoefficient(*rho_coeff);
zero_vacuum_density(f, rho_grid);
analysis::conserve_mass(f, rho_grid, mass);
f.com = analysis::get_com(f, rho_grid);
f.Q = physics::compute_quadrupole_moment_tensor(f, rho_grid, f.com);
const auto gravity_solution = physics::solve_gravity_field(f, args, rho_grid, displacement);
const int quadrature_order = get_gravity_quadrature_order(f);
const GravitationalEnergies energies =
compute_gravitational_energies(f, rho_grid, gravity_solution, quadrature_order);
REQUIRE(energies.binding < 0.0);
REQUIRE(energies.virial < 0.0);
const double relative_consistency_error = std::abs(energies.binding - energies.virial) / std::abs(energies.binding);
INFO("W_bind = " << energies.binding);
INFO("W_vir = " << energies.virial);
INFO("Relative virial consistency error = " << relative_consistency_error);
CHECK_THAT(relative_consistency_error, Catch::Matchers::WithinAbs(0.0, rational_profile_virial_tolerance));
}
TEST_CASE(
"Homogeneous Ellipsoid Analytic Gravity",
tags::gravity_analytic
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
const double radius = utils::RADIUS;
const double mass = utils::MASS;
constexpr double x_scale = 1.15;
constexpr double y_scale = 0.95;
constexpr double z_scale = 1.0 / (x_scale * y_scale);
assert(std::abs(x_scale * y_scale * z_scale - 1.0) < 1.0e-14);
const double semi_axis_x = x_scale * radius;
const double semi_axis_y = y_scale * radius;
const double semi_axis_z = z_scale * radius;
auto affine_displacement = [](const mfem::Vector &x, mfem::Vector &displacement_value) {
displacement_value.SetSize(3);
displacement_value(0) = (x_scale - 1.0) * x(0);
displacement_value(1) = (y_scale - 1.0) * x(1);
displacement_value(2) = (z_scale - 1.0) * x(2);
};
mfem::VectorFunctionCoefficient displacement_coeff(3, affine_displacement);
mfem::ParGridFunction displacement(f.displacementFes.get());
displacement.ProjectCoefficient(displacement_coeff);
*f.displacement = displacement;
const double analytic_volume = (4.0 / 3.0) * M_PI * semi_axis_x * semi_axis_y * semi_axis_z;
const double density = mass / analytic_volume;
mfem::GridFunction rho_grid(f.densityFes.get());
rho_grid = density;
zero_vacuum_density(f, rho_grid);
const double projected_mass = analysis::domain_integrate_grid_function(f, rho_grid, utils::DOMAINS::STELLAR);
const double numerical_density = density * mass / projected_mass;
analysis::conserve_mass(f, rho_grid, mass);
f.com = analysis::get_com(f, rho_grid);
f.Q = physics::compute_quadrupole_moment_tensor(f, rho_grid, f.com);
const HomogeneousEllipsoidAnalytic analytic =
compute_homogeneous_ellipsoid_analytic(semi_axis_x, semi_axis_y, semi_axis_z);
const double coefficient_sum = analytic.coefficient_x + analytic.coefficient_y + analytic.coefficient_z;
INFO("A_x = " << analytic.coefficient_x);
INFO("A_y = " << analytic.coefficient_y);
INFO("A_z = " << analytic.coefficient_z);
INFO("A_x + A_y + A_z = " << coefficient_sum);
REQUIRE_THAT(coefficient_sum, Catch::Matchers::WithinAbs(2.0, 1.0e-11));
mfem::DenseMatrix analytic_quadrupole(3, 3);
analytic_quadrupole = 0.0;
analytic_quadrupole(0, 0) =
(mass / 5.0) * (2.0 * semi_axis_x * semi_axis_x - semi_axis_y * semi_axis_y - semi_axis_z * semi_axis_z);
analytic_quadrupole(1, 1) =
(mass / 5.0) * (2.0 * semi_axis_y * semi_axis_y - semi_axis_x * semi_axis_x - semi_axis_z * semi_axis_z);
analytic_quadrupole(2, 2) =
(mass / 5.0) * (2.0 * semi_axis_z * semi_axis_z - semi_axis_x * semi_axis_x - semi_axis_y * semi_axis_y);
mfem::DenseMatrix quadrupole_difference(f.Q);
quadrupole_difference -= analytic_quadrupole;
const double relative_quadrupole_error = quadrupole_difference.FNorm() / analytic_quadrupole.FNorm();
INFO("Relative quadrupole error = " << relative_quadrupole_error);
HomogeneousEllipsoidHDivCoefficient analytic_field_coefficient(
*f.domainMapperStateless, *f.displacement, *f.compactificationCoordinate, numerical_density, analytic
);
mfem::ParGridFunction analytic_field_projection(f.gravityFluxFes.get());
analytic_field_projection = 0.0;
for (int i = 0; i < f.mesh->attributes.Size(); ++i) {
const int attribute = f.mesh->attributes[i];
if (attribute != 3) {
analytic_field_projection.ProjectCoefficient(analytic_field_coefficient, attribute);
}
}
const auto gravity_solution = physics::solve_gravity_field(f, args, rho_grid, displacement);
const int quadrature_order = get_gravity_quadrature_order(f);
double local_field_error_squared = 0.0;
double local_field_norm_squared = 0.0;
double local_projection_error_squared = 0.0;
double local_gravity_projection_difference_squared = 0.0;
mfem::Vector x_physical(3);
mfem::Vector grad_phi_element(3);
mfem::Vector grad_phi_physical(3);
mfem::Vector grad_phi_analytic(3);
mfem::Vector grad_phi_difference(3);
mfem::Vector projected_field_element(3);
mfem::Vector projected_field_physical(3);
mfem::Vector projected_field_difference(3);
mfem::Vector gravity_projection_difference(3);
mapping::GridFunctionMappingEvaluator mapping_evaluator(
*f.domainMapperStateless, *f.displacement, *f.compactificationCoordinate
);
for (int elem_id = 0; elem_id < f.mesh->GetNE(); ++elem_id) {
if (f.mesh->GetAttribute(elem_id) == 3) {
continue;
}
mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(elem_id);
const mfem::IntegrationRule &integration_rule =
mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order);
for (int q = 0; q < integration_rule.GetNPoints(); ++q) {
const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q);
transformation->SetIntPoint(&integration_point);
mapping::VolumeMappingContext mapping_context;
MFEM_VERIFY(
mapping_evaluator.EvaluateVolume(*transformation, integration_point, mapping_context) ==
mapping::MappingStatus::valid,
"Ellipsoid field comparison encountered an invalid mapping."
);
const double map_determinant = mapping_context.mapping.mapping_determinant;
const double weight = mapping_context.quadrature.weight;
x_physical = mapping_context.mapping.physical_position;
gravity_solution.gradPhi.GetVectorValue(elem_id, integration_point, grad_phi_element);
const mfem::DenseMatrix &map_jacobian = mapping_context.mapping.mapping_jacobian;
map_jacobian.Mult(grad_phi_element, grad_phi_physical);
grad_phi_physical /= map_determinant;
analytic_field_projection.GetVectorValue(elem_id, integration_point, projected_field_element);
map_jacobian.Mult(projected_field_element, projected_field_physical);
projected_field_physical /= map_determinant;
grad_phi_analytic(0) = 2.0 * M_PI * utils::G * numerical_density * analytic.coefficient_x * x_physical(0);
grad_phi_analytic(1) = 2.0 * M_PI * utils::G * numerical_density * analytic.coefficient_y * x_physical(1);
grad_phi_analytic(2) = 2.0 * M_PI * utils::G * numerical_density * analytic.coefficient_z * x_physical(2);
projected_field_difference = projected_field_physical;
projected_field_difference -= grad_phi_analytic;
local_projection_error_squared += (projected_field_difference * projected_field_difference) * weight;
gravity_projection_difference = grad_phi_physical;
gravity_projection_difference -= projected_field_physical;
local_gravity_projection_difference_squared +=
(gravity_projection_difference * gravity_projection_difference) * weight;
grad_phi_difference = grad_phi_physical;
grad_phi_difference -= grad_phi_analytic;
local_field_error_squared += (grad_phi_difference * grad_phi_difference) * weight;
local_field_norm_squared += (grad_phi_analytic * grad_phi_analytic) * weight;
}
}
double global_field_error_squared = 0.0;
double global_field_norm_squared = 0.0;
double global_projection_error_squared = 0.0;
double global_gravity_projection_difference_squared = 0.0;
MPI_Comm communicator = f.densityFes->GetComm();
MPI_Allreduce(&local_field_error_squared, &global_field_error_squared, 1, MPI_DOUBLE, MPI_SUM, communicator);
MPI_Allreduce(&local_field_norm_squared, &global_field_norm_squared, 1, MPI_DOUBLE, MPI_SUM, communicator);
MPI_Allreduce(
&local_projection_error_squared, &global_projection_error_squared, 1, MPI_DOUBLE, MPI_SUM, communicator
);
MPI_Allreduce(
&local_gravity_projection_difference_squared, &global_gravity_projection_difference_squared, 1, MPI_DOUBLE,
MPI_SUM, communicator
);
const double relative_field_error = std::sqrt(global_field_error_squared / global_field_norm_squared);
const GravitationalEnergies energies =
compute_gravitational_energies(f, rho_grid, gravity_solution, quadrature_order);
const double analytic_binding_energy = -(3.0 / 10.0) * utils::G * mass * mass * analytic.energy_kernel;
const double relative_binding_energy_error =
std::abs(energies.binding - analytic_binding_energy) / std::abs(analytic_binding_energy);
const double relative_virial_energy_error =
std::abs(energies.virial - analytic_binding_energy) / std::abs(analytic_binding_energy);
const double relative_consistency_error = std::abs(energies.binding - energies.virial) / std::abs(energies.binding);
const double relative_projection_error = std::sqrt(global_projection_error_squared / global_field_norm_squared);
const double gravity_to_projection_error_ratio = relative_projection_error > 0.0
? relative_field_error / relative_projection_error
: std::numeric_limits<double>::infinity();
const double relative_gravity_projection_difference =
std::sqrt(global_gravity_projection_difference_squared / global_field_norm_squared);
const double projection_gap_ratio = relative_gravity_projection_difference / relative_projection_error;
INFO("Analytic binding energy = " << analytic_binding_energy);
INFO("Computed binding energy = " << energies.binding);
INFO("Computed virial energy = " << energies.virial);
INFO("Relative field L2 error = " << relative_field_error);
INFO("Relative binding energy error = " << relative_binding_energy_error);
INFO("Relative virial energy error = " << relative_virial_energy_error);
INFO("Relative virial consistency error = " << relative_consistency_error);
INFO("Relative gravity-to-RT-projection difference = " << relative_gravity_projection_difference);
INFO("Gravity-to-projection gap ratio = " << projection_gap_ratio);
INFO("Relative RT projection L2 error = " << relative_projection_error);
INFO("Gravity-to-projection error ratio = " << gravity_to_projection_error_ratio);
REQUIRE(std::isfinite(relative_projection_error));
// The RT projection itself is accurate to approximately 8.8e-4 on this
// mesh. The solved field should remain close to that best representable
// field, while the integrated energies have a substantially lower floor.
constexpr double quadrupole_tolerance = 2.0e-4;
constexpr double field_tolerance = 1.0e-3;
constexpr double binding_energy_tolerance = 1.0e-5;
constexpr double virial_energy_tolerance = 5.0e-5;
constexpr double consistency_tolerance = 5.0e-5;
constexpr double projection_gap_tolerance = 0.3;
constexpr double projection_ratio_tolerance = 1.05;
CHECK_THAT(relative_quadrupole_error, Catch::Matchers::WithinAbs(0.0, quadrupole_tolerance));
CHECK_THAT(relative_field_error, Catch::Matchers::WithinAbs(0.0, field_tolerance));
CHECK_THAT(relative_binding_energy_error, Catch::Matchers::WithinAbs(0.0, binding_energy_tolerance));
CHECK_THAT(relative_virial_energy_error, Catch::Matchers::WithinAbs(0.0, virial_energy_tolerance));
CHECK_THAT(relative_consistency_error, Catch::Matchers::WithinAbs(0.0, consistency_tolerance));
CHECK(projection_gap_ratio < projection_gap_tolerance);
CHECK(gravity_to_projection_error_ratio < projection_ratio_tolerance);
}
TEST_CASE(
"Deformed Rational Density Virial Self-Consistency",
tags::gravity_consistency
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
const double radius = utils::RADIUS;
const double mass = utils::MASS;
constexpr double x_scale = 1.15;
constexpr double y_scale = 0.95;
constexpr double z_scale = 1.0 / (x_scale * y_scale);
assert(std::abs(x_scale * y_scale * z_scale - 1.0) < 1.0e-14);
const double semi_axis_x = x_scale * radius;
const double semi_axis_y = y_scale * radius;
const double semi_axis_z = z_scale * radius;
auto affine_displacement = [](const mfem::Vector &x, mfem::Vector &displacement_value) {
displacement_value.SetSize(3);
displacement_value(0) = (x_scale - 1.0) * x(0);
displacement_value(1) = (y_scale - 1.0) * x(1);
displacement_value(2) = (z_scale - 1.0) * x(2);
};
mfem::VectorFunctionCoefficient displacement_coeff(3, affine_displacement);
mfem::ParGridFunction displacement(f.displacementFes.get());
displacement.ProjectCoefficient(displacement_coeff);
*f.displacement = displacement;
constexpr double concentration = 16.0;
const double density_scale = mass / std::pow(radius, 3.0);
auto ellipsoidal_rho = [semi_axis_x, semi_axis_y, semi_axis_z, density_scale](const mfem::Vector &x) {
const double ellipsoidal_radius_squared = x(0) * x(0) / (semi_axis_x * semi_axis_x) +
x(1) * x(1) / (semi_axis_y * semi_axis_y) +
x(2) * x(2) / (semi_axis_z * semi_axis_z);
if (ellipsoidal_radius_squared >= 1.0) {
return 0.0;
}
const double denominator = 1.0 + concentration * ellipsoidal_radius_squared;
return density_scale * (1.0 - ellipsoidal_radius_squared) / (denominator * denominator);
};
std::unique_ptr<mfem::Coefficient> rho_coeff;
if (f.has_mapping()) {
rho_coeff = std::make_unique<mapping::PhysicalPositionFunctionCoefficient>(
*f.domainMapperStateless, *f.displacement, *f.compactificationCoordinate, ellipsoidal_rho
);
} else {
rho_coeff = std::make_unique<mfem::FunctionCoefficient>(ellipsoidal_rho);
}
mfem::GridFunction rho_grid(f.densityFes.get());
rho_grid.ProjectCoefficient(*rho_coeff);
zero_vacuum_density(f, rho_grid);
analysis::conserve_mass(f, rho_grid, mass);
f.com = analysis::get_com(f, rho_grid);
f.Q = physics::compute_quadrupole_moment_tensor(f, rho_grid, f.com);
const double normalized_quadrupole = f.Q.FNorm() / (mass * radius * radius);
INFO("Normalized quadrupole = " << normalized_quadrupole);
REQUIRE(normalized_quadrupole > 1.0e-3);
const auto gravity_solution = physics::solve_gravity_field(f, args, rho_grid, displacement);
const int quadrature_order = get_gravity_quadrature_order(f);
const GravitationalEnergies energies =
compute_gravitational_energies(f, rho_grid, gravity_solution, quadrature_order);
REQUIRE(energies.binding < 0.0);
REQUIRE(energies.virial < 0.0);
const double relative_consistency_error = std::abs(energies.binding - energies.virial) / std::abs(energies.binding);
INFO("W_bind = " << energies.binding);
INFO("W_vir = " << energies.virial);
INFO("Relative virial consistency error = " << relative_consistency_error);
CHECK_THAT(relative_consistency_error, Catch::Matchers::WithinAbs(0.0, rational_profile_virial_tolerance));
}
TEST_CASE(
"Gravity Field Matches Uniform Sphere Analytic",
tags::gravity_analytic
) {
auto args = test_utils::setup_args();
args.p.rtol = 1.0e-13;
args.p.max_iters = std::max(args.p.max_iters, 1000);
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
*f.displacement = 0.0;
mfem::ParGridFunction displacement(f.displacementFes.get());
displacement = 0.0;
const double radius = utils::RADIUS;
const double mass = utils::MASS;
const double analytic_volume = (4.0 / 3.0) * M_PI * std::pow(radius, 3.0);
const double density = mass / analytic_volume;
mfem::GridFunction rho_uniform(f.densityFes.get());
rho_uniform = density;
zero_vacuum_density(f, rho_uniform);
analysis::conserve_mass(f, rho_uniform, mass);
f.com = analysis::get_com(f, rho_uniform);
f.Q = physics::compute_quadrupole_moment_tensor(f, rho_uniform, f.com);
const physics::GravitySolution gravity_solution = physics::solve_gravity_field(f, args, rho_uniform, displacement);
constexpr double potential_tolerance = utils::APPROX_MAX_ACCEPTABLE_POTENTIAL_ERROR_SI_BURNING;
double local_maximum_absolute_error = 0.0;
double local_maximum_relative_error = 0.0;
mapping::GridFunctionMappingEvaluator mapping_evaluator(
*f.domainMapperStateless, *f.displacement, *f.compactificationCoordinate
);
const int elements_to_test = std::min(30, f.mesh->GetNE());
for (int element_id = 0; element_id < elements_to_test; ++element_id) {
if (f.mesh->GetAttribute(element_id) == 3) {
continue;
}
mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(element_id);
const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get(transformation->GetGeometryType(), 2);
const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(0);
transformation->SetIntPoint(&integration_point);
mfem::Vector physical_position;
mapping_evaluator.GetPhysicalPoint(*transformation, integration_point, physical_position);
const double radial_coordinate = physical_position.Norml2();
if (radial_coordinate < 1.0e-9) {
continue;
}
const double analytic_potential = -(utils::G * mass / (2.0 * std::pow(radius, 3.0))) *
(3.0 * radius * radius - radial_coordinate * radial_coordinate);
const double computed_potential = gravity_solution.phi.GetValue(element_id, integration_point);
const double absolute_error = std::abs(computed_potential - analytic_potential);
const double relative_error = absolute_error / std::abs(analytic_potential);
local_maximum_absolute_error = std::max(local_maximum_absolute_error, absolute_error);
local_maximum_relative_error = std::max(local_maximum_relative_error, relative_error);
}
double global_maximum_absolute_error = 0.0;
double global_maximum_relative_error = 0.0;
MPI_Allreduce(
&local_maximum_absolute_error, &global_maximum_absolute_error, 1, MPI_DOUBLE, MPI_MAX, f.densityFes->GetComm()
);
MPI_Allreduce(
&local_maximum_relative_error, &global_maximum_relative_error, 1, MPI_DOUBLE, MPI_MAX, f.densityFes->GetComm()
);
const int quadrature_order = get_gravity_quadrature_order(f);
const GravitationalEnergies energies =
compute_gravitational_energies(f, rho_uniform, gravity_solution, quadrature_order);
const double analytic_binding_energy = -(3.0 / 5.0) * utils::G * mass * mass / radius;
const double relative_binding_error =
std::abs(energies.binding - analytic_binding_energy) / std::abs(analytic_binding_energy);
const double relative_virial_error =
std::abs(energies.virial - analytic_binding_energy) / std::abs(analytic_binding_energy);
const double relative_consistency_error = std::abs(energies.binding - energies.virial) / std::abs(energies.binding);
INFO("Global maximum absolute potential error = " << global_maximum_absolute_error);
INFO("Global maximum relative potential error = " << global_maximum_relative_error);
INFO("Analytic binding energy = " << analytic_binding_energy);
INFO("New-solver binding energy = " << energies.binding);
INFO("New-solver virial energy = " << energies.virial);
INFO("Relative binding-energy error = " << relative_binding_error);
INFO("Relative virial-energy error = " << relative_virial_error);
INFO("Relative virial consistency error = " << relative_consistency_error);
REQUIRE(energies.binding < 0.0);
REQUIRE(energies.virial < 0.0);
constexpr double energy_tolerance = 1.0e-5;
constexpr double consistency_tolerance = 1.0e-6;
CHECK_THAT(global_maximum_relative_error, Catch::Matchers::WithinAbs(0.0, 0.1 * potential_tolerance));
CHECK_THAT(global_maximum_absolute_error, Catch::Matchers::WithinAbs(0.0, 0.1 * potential_tolerance));
CHECK_THAT(relative_binding_error, Catch::Matchers::WithinAbs(0.0, energy_tolerance));
CHECK_THAT(relative_virial_error, Catch::Matchers::WithinAbs(0.0, energy_tolerance));
CHECK_THAT(relative_consistency_error, Catch::Matchers::WithinAbs(0.0, consistency_tolerance));
}
TEST_CASE(
"Gravity Field Deformed Rational Density Virial Self-Consistency",
tags::gravity_consistency
) {
auto args = test_utils::setup_args();
args.p.rtol = 1.0e-13;
args.p.max_iters = std::max(args.p.max_iters, 1000);
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
const double radius = utils::RADIUS;
const double mass = utils::MASS;
constexpr double x_scale = 1.15;
constexpr double y_scale = 0.95;
constexpr 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));
const double semi_axis_x = x_scale * radius;
const double semi_axis_y = y_scale * radius;
const double semi_axis_z = z_scale * radius;
auto affine_displacement = [](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, affine_displacement);
mfem::ParGridFunction displacement(f.displacementFes.get());
displacement.ProjectCoefficient(displacement_coefficient);
*f.displacement = displacement;
constexpr double concentration = 16.0;
const double density_scale = mass / std::pow(radius, 3.0);
auto ellipsoidal_density = [semi_axis_x, semi_axis_y, semi_axis_z, density_scale](const mfem::Vector &position) {
const double ellipsoidal_radius_squared = position(0) * position(0) / (semi_axis_x * semi_axis_x) +
position(1) * position(1) / (semi_axis_y * semi_axis_y) +
position(2) * position(2) / (semi_axis_z * semi_axis_z);
if (ellipsoidal_radius_squared >= 1.0) {
return 0.0;
}
const double denominator = 1.0 + concentration * ellipsoidal_radius_squared;
return density_scale * (1.0 - ellipsoidal_radius_squared) / (denominator * denominator);
};
mapping::PhysicalPositionFunctionCoefficient density_coefficient(
*f.domainMapperStateless, *f.displacement, *f.compactificationCoordinate, ellipsoidal_density
);
mfem::GridFunction density(f.densityFes.get());
density.ProjectCoefficient(density_coefficient);
zero_vacuum_density(f, density);
analysis::conserve_mass(f, density, mass);
f.com = analysis::get_com(f, density);
f.Q = physics::compute_quadrupole_moment_tensor(f, density, f.com);
const double normalized_quadrupole = f.Q.FNorm() / (mass * radius * radius);
INFO("Normalized quadrupole = " << normalized_quadrupole);
REQUIRE(normalized_quadrupole > 1.0e-3);
const physics::GravitySolution gravity_solution = physics::solve_gravity_field(f, args, density, displacement);
const int base_quadrature_order = get_gravity_quadrature_order(f);
const GravitationalEnergies base_energies =
compute_gravitational_energies(f, density, gravity_solution, base_quadrature_order);
const GravitationalEnergies medium_energies =
compute_gravitational_energies(f, density, gravity_solution, base_quadrature_order + 4);
const GravitationalEnergies fine_energies =
compute_gravitational_energies(f, density, gravity_solution, base_quadrature_order + 8);
REQUIRE(fine_energies.binding < 0.0);
REQUIRE(fine_energies.virial < 0.0);
const double base_consistency_error =
std::abs(base_energies.binding - base_energies.virial) / std::abs(base_energies.binding);
const double medium_consistency_error =
std::abs(medium_energies.binding - medium_energies.virial) / std::abs(medium_energies.binding);
const double fine_consistency_error =
std::abs(fine_energies.binding - fine_energies.virial) / std::abs(fine_energies.binding);
const double binding_quadrature_change =
std::abs(fine_energies.binding - medium_energies.binding) / std::abs(fine_energies.binding);
const double virial_quadrature_change =
std::abs(fine_energies.virial - medium_energies.virial) / std::abs(fine_energies.virial);
INFO("Base-order consistency error = " << base_consistency_error);
INFO("Medium-order consistency error = " << medium_consistency_error);
INFO("Fine-order consistency error = " << fine_consistency_error);
INFO("Medium-to-fine binding-energy change = " << binding_quadrature_change);
INFO("Medium-to-fine virial-energy change = " << virial_quadrature_change);
constexpr double diagnostic_quadrature_tolerance = 1.0e-7;
CHECK_THAT(fine_consistency_error, Catch::Matchers::WithinAbs(0.0, rational_profile_virial_tolerance));
CHECK_THAT(binding_quadrature_change, Catch::Matchers::WithinAbs(0.0, diagnostic_quadrature_tolerance));
CHECK_THAT(virial_quadrature_change, Catch::Matchers::WithinAbs(0.0, diagnostic_quadrature_tolerance));
}
TEST_CASE(
"Exterior Monopole Error Is Separated From Finite Element 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);
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.domainMapperStateless != nullptr);
REQUIRE(f.domainMapperStateless != nullptr);
REQUIRE(f.compactificationCoordinate != nullptr);
const double stellar_radius = utils::RADIUS;
const double mass = utils::MASS;
const double analytic_volume = (4.0 / 3.0) * M_PI * stellar_radius * stellar_radius * stellar_radius;
const double density = mass / analytic_volume;
mfem::ParGridFunction displacement(f.displacementFes.get());
displacement = 0.0;
*f.displacement = 0.0;
mfem::GridFunction rho_uniform(f.densityFes.get());
rho_uniform = density;
zero_vacuum_density(f, rho_uniform);
analysis::conserve_mass(f, rho_uniform, mass);
f.com = analysis::get_com(f, rho_uniform);
f.Q = physics::compute_quadrupole_moment_tensor(f, rho_uniform, f.com);
const physics::GravitySolution numerical_solution =
physics::solve_gravity_field(f, args, rho_uniform, displacement);
StatelessMonopolePotentialCoefficient analytic_potential_coefficient(
f, *f.domainMapperStateless, displacement, mass, stellar_radius
);
StatelessMonopoleHDivCoefficient analytic_field_coefficient(
f, *f.domainMapperStateless, displacement, mass, stellar_radius
);
physics::GravitySolution analytic_projection(f);
analytic_projection.phi = 0.0;
analytic_projection.gradPhi = 0.0;
analytic_projection.phi.ProjectCoefficient(analytic_potential_coefficient);
analytic_projection.gradPhi.ProjectCoefficient(analytic_field_coefficient);
const std::array<ExteriorMonopoleShellMetrics, 5> numerical_metrics =
measure_exterior_monopole_shells(f, numerical_solution, displacement, mass);
const std::array<ExteriorMonopoleShellMetrics, 5> projection_metrics =
measure_exterior_monopole_shells(f, analytic_projection, displacement, mass);
mfem::Vector numerical_gradient_true;
mfem::Vector numerical_potential_true;
mfem::Vector projected_gradient_true;
mfem::Vector projected_potential_true;
numerical_solution.gradPhi.GetTrueDofs(numerical_gradient_true);
numerical_solution.phi.GetTrueDofs(numerical_potential_true);
analytic_projection.gradPhi.GetTrueDofs(projected_gradient_true);
analytic_projection.phi.GetTrueDofs(projected_potential_true);
MPI_Comm communicator = f.gravityFluxFes->GetComm();
const double gradient_projection_gap =
global_relative_vector_error(numerical_gradient_true, projected_gradient_true, communicator);
const double potential_projection_gap =
global_relative_vector_error(numerical_potential_true, projected_potential_true, 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_field = 0.0;
double maximum_projected_tangential_field = 0.0;
std::ostringstream report;
report << "Global numerical/projection gradient DOF gap = " << gradient_projection_gap << '\n'
<< "Global numerical/projection potential DOF gap = " << potential_projection_gap << '\n';
for (int shell = 0; shell < 5; ++shell) {
const ExteriorMonopoleShellMetrics &numerical = numerical_metrics[shell];
const ExteriorMonopoleShellMetrics &projected = projection_metrics[shell];
maximum_numerical_potential_error = std::max(maximum_numerical_potential_error, numerical.potential_rms_error);
maximum_projected_potential_error = std::max(maximum_projected_potential_error, projected.potential_rms_error);
maximum_numerical_radial_error = std::max(maximum_numerical_radial_error, numerical.radial_field_rms_error);
maximum_projected_radial_error = std::max(maximum_projected_radial_error, projected.radial_field_rms_error);
maximum_numerical_tangential_field =
std::max(maximum_numerical_tangential_field, numerical.tangential_field_rms);
maximum_projected_tangential_field =
std::max(maximum_projected_tangential_field, projected.tangential_field_rms);
report << "Shell " << shell << " xi=[" << exterior_shell_boundaries[shell] << ", "
<< exterior_shell_boundaries[shell + 1] << "):\n"
<< " radius range = [" << numerical.minimum_radius << ", " << numerical.maximum_radius << "]\n"
<< " numerical potential error = " << numerical.potential_rms_error << '\n'
<< " projected potential error = " << projected.potential_rms_error << '\n'
<< " numerical radial-field error = " << numerical.radial_field_rms_error << '\n'
<< " projected radial-field error = " << projected.radial_field_rms_error << '\n'
<< " numerical tangential field = " << numerical.tangential_field_rms << '\n'
<< " projected tangential field = " << projected.tangential_field_rms << '\n';
}
INFO(report.str());
REQUIRE(std::isfinite(gradient_projection_gap));
REQUIRE(std::isfinite(potential_projection_gap));
REQUIRE(maximum_numerical_potential_error > 0.0);
REQUIRE(maximum_projected_potential_error > 0.0);
REQUIRE(maximum_numerical_radial_error > 0.0);
REQUIRE(maximum_projected_radial_error > 0.0);
/*
* Broad guards against a broken projection. These are not the final
* physical acceptance thresholds.
*/
CHECK(maximum_projected_potential_error < 5.0e-2);
CHECK(maximum_projected_radial_error < 5.0e-3);
CHECK(maximum_projected_tangential_field < 5.0e-3);
CHECK(maximum_numerical_potential_error < 5.0e-2);
CHECK(maximum_numerical_radial_error < 5.0e-3);
CHECK(maximum_numerical_tangential_field < 5.0e-3);
/*
* These are the decisive comparisons. If either fails, the solved
* field is farther from the direct FE representation than it is from
* the continuum monopole, indicating an operator-consistency issue
* rather than a simple approximation floor.
*/
CHECK(gradient_projection_gap < maximum_numerical_radial_error);
CHECK(potential_projection_gap < maximum_numerical_potential_error);
}
TEST_CASE(
"Gravity Field Matches Analytic Interior Potential For A Deformed "
"Homogeneous Star",
tags::gravity_analytic_accuracy
) {
auto args = test_utils::setup_args();
args.p.rtol = 1.0e-13;
args.p.atol = 1.0e-14;
args.p.max_iters = std::max(args.p.max_iters, 2000);
fem::FEM fem = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(fem.domainMapperStateless != nullptr);
REQUIRE(fem.domainMapperStateless != nullptr);
const double radius = utils::RADIUS;
const double mass = utils::MASS;
constexpr double x_scale = 1.15;
constexpr double y_scale = 0.95;
constexpr double z_scale = 1.0 / (x_scale * y_scale);
const double semi_axis_x = x_scale * radius;
const double semi_axis_y = y_scale * radius;
const double semi_axis_z = z_scale * radius;
REQUIRE_THAT(x_scale * y_scale * z_scale, Catch::Matchers::WithinAbs(1.0, 1.0e-14));
auto displacement_function = [](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(fem.displacementFes.get());
displacement.ProjectCoefficient(displacement_coefficient);
*fem.displacement = displacement;
const double analytic_volume = (4.0 / 3.0) * M_PI * semi_axis_x * semi_axis_y * semi_axis_z;
const double density_value = mass / analytic_volume;
mfem::GridFunction density(fem.densityFes.get());
density = density_value;
zero_vacuum_density(fem, density);
const double projected_mass = analysis::domain_integrate_grid_function(fem, density, utils::DOMAINS::STELLAR);
const double numerical_density = density_value * mass / projected_mass;
analysis::conserve_mass(fem, density, mass);
fem.com = analysis::get_com(fem, density);
fem.Q = physics::compute_quadrupole_moment_tensor(fem, density, fem.com);
const HomogeneousEllipsoidAnalytic analytic =
compute_homogeneous_ellipsoid_analytic(semi_axis_x, semi_axis_y, semi_axis_z);
auto analytic_potential = [numerical_density, semi_axis_x, semi_axis_y, semi_axis_z,
analytic](const mfem::Vector &position) {
const double potential_kernel = semi_axis_x * semi_axis_y * semi_axis_z * analytic.energy_kernel -
analytic.coefficient_x * position(0) * position(0) -
analytic.coefficient_y * position(1) * position(1) -
analytic.coefficient_z * position(2) * position(2);
return -M_PI * utils::G * numerical_density * potential_kernel;
};
mapping::PhysicalPositionFunctionCoefficient analytic_potential_coefficient(
*fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate, analytic_potential
);
const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute;
mfem::ParGridFunction projected_potential(fem.gravityPotentialFes.get());
projected_potential.ProjectCoefficient(analytic_potential_coefficient);
const physics::GravitySolution solution = physics::solve_gravity_field(fem, args, density, displacement);
const int quadrature_order = get_gravity_quadrature_order(fem);
double local_solution_error_squared = 0.0;
double local_projection_error_squared = 0.0;
double local_solution_projection_gap_squared = 0.0;
double local_analytic_norm_squared = 0.0;
double local_projected_norm_squared = 0.0;
double local_maximum_relative_error = 0.0;
mfem::Vector physical_position(3);
mapping::GridFunctionMappingEvaluator mapping_evaluator(
*fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate
);
for (int element_id = 0; element_id < fem.mesh->GetNE(); ++element_id) {
mfem::ElementTransformation *transformation = fem.mesh->GetElementTransformation(element_id);
if (transformation->Attribute == vacuum_attribute) {
continue;
}
const mfem::IntegrationRule &rule = mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order);
for (int quadrature_point_id = 0; quadrature_point_id < rule.GetNPoints(); ++quadrature_point_id) {
const mfem::IntegrationPoint &point = rule.IntPoint(quadrature_point_id);
transformation->SetIntPoint(&point);
mapping::MappingPointContext mapping_context;
MFEM_VERIFY(
mapping_evaluator.EvaluatePoint(*transformation, point, mapping_context) ==
mapping::MappingStatus::valid,
"Deformed potential test encountered an invalid mapping."
);
physical_position = mapping_context.physical_position;
const double mapping_determinant = mapping_context.mapping_determinant;
MFEM_VERIFY(
mapping_determinant > 0.0, "Deformed potential test encountered a "
"non-positive mapping determinant."
);
const double expected_potential = analytic_potential(physical_position);
const double computed_potential = solution.phi.GetValue(element_id, point);
const double projected_potential_value = projected_potential.GetValue(element_id, point);
const double weight = point.weight * transformation->Weight() * mapping_determinant;
local_solution_error_squared +=
weight * (computed_potential - expected_potential) * (computed_potential - expected_potential);
local_projection_error_squared += weight * (projected_potential_value - expected_potential) *
(projected_potential_value - expected_potential);
local_solution_projection_gap_squared += weight * (computed_potential - projected_potential_value) *
(computed_potential - projected_potential_value);
local_analytic_norm_squared += weight * expected_potential * expected_potential;
local_projected_norm_squared += weight * projected_potential_value * projected_potential_value;
local_maximum_relative_error = std::max(
local_maximum_relative_error,
std::abs(computed_potential - expected_potential) /
std::max(std::abs(expected_potential), std::numeric_limits<double>::epsilon())
);
}
}
const std::array<double, 5> local_values{
local_solution_error_squared, local_projection_error_squared, local_solution_projection_gap_squared,
local_analytic_norm_squared, local_projected_norm_squared
};
std::array<double, 5> global_values{};
MPI_Allreduce(
local_values.data(), global_values.data(), static_cast<int>(local_values.size()), MPI_DOUBLE, MPI_SUM,
fem.densityFes->GetComm()
);
double maximum_relative_error = 0.0;
MPI_Allreduce(
&local_maximum_relative_error, &maximum_relative_error, 1, MPI_DOUBLE, MPI_MAX, fem.densityFes->GetComm()
);
const double solution_relative_error = std::sqrt(global_values[0] / global_values[3]);
const double projection_relative_error = std::sqrt(global_values[1] / global_values[3]);
const double solution_projection_gap = std::sqrt(global_values[2] / global_values[4]);
INFO(
"Ellipsoid coefficients = (" << analytic.coefficient_x << ", " << analytic.coefficient_y << ", "
<< analytic.coefficient_z << ")"
);
INFO("Analytic interior-potential L2 relative error = " << solution_relative_error);
INFO("Analytic-potential FE projection L2 relative error = " << projection_relative_error);
INFO("New-solver / analytic-potential projection relative gap = " << solution_projection_gap);
INFO("Maximum interior pointwise relative potential error = " << maximum_relative_error);
REQUIRE(std::isfinite(solution_relative_error));
REQUIRE(std::isfinite(projection_relative_error));
REQUIRE(std::isfinite(solution_projection_gap));
REQUIRE(std::isfinite(maximum_relative_error));
/*
* On the regression mesh, the direct L2 projection floor is about 6.7e-6,
* the mixed-solve/projection gap is about 3.3e-5, and the maximum
* pointwise error is about 2e-4. These bounds guard those independently.
*/
CHECK(solution_relative_error < 5.0e-5);
CHECK(projection_relative_error < 1.0e-5);
CHECK(solution_projection_gap < 5.0e-5);
CHECK(maximum_relative_error < 2.5e-4);
}
struct FerrersN1Analytic {
double potential_constant;
std::array<double, 3> first_coefficients;
std::array<std::array<double, 3>, 3> second_coefficients;
};
class FerrersVacuumMaskedCoefficient final : public mfem::Coefficient {
public:
FerrersVacuumMaskedCoefficient(
Coefficient &coefficient,
const int vacuum_attribute
)
: m_coefficient(coefficient),
m_vacuum_attribute(vacuum_attribute) {
}
double Eval(
mfem::ElementTransformation &transformation,
const mfem::IntegrationPoint &integration_point
) override {
if (transformation.Attribute == m_vacuum_attribute) {
return 0.0;
}
return m_coefficient.Eval(transformation, integration_point);
}
private:
Coefficient &m_coefficient;
int m_vacuum_attribute;
};
static double compute_ferrers_n1_coefficient(
const std::array<
double,
3> &semi_axes,
const int first_denominator_axis,
const int second_denominator_axis
) {
const double axis_product = semi_axes[0] * semi_axes[1] * semi_axes[2];
const double length_scale = std::cbrt(axis_product);
auto integrand = [semi_axes, axis_product, length_scale, first_denominator_axis,
second_denominator_axis](const double t) {
if (t <= 0.0 || t >= 1.0) {
return 0.0;
}
/*
* Map u in [0, infinity) to t in [0, 1]:
*
* u = L^2 [t / (1 - t)]^2.
*/
const double one_minus_t = 1.0 - t;
const double s = t / one_minus_t;
const double u = length_scale * length_scale * s * s;
const double du_dt = length_scale * length_scale * 2.0 * s / (one_minus_t * one_minus_t);
const double delta = std::sqrt(
(semi_axes[0] * semi_axes[0] + u) * (semi_axes[1] * semi_axes[1] + u) * (semi_axes[2] * semi_axes[2] + u)
);
double value = axis_product * du_dt / delta;
if (first_denominator_axis >= 0) {
value /= semi_axes[first_denominator_axis] * semi_axes[first_denominator_axis] + u;
}
if (second_denominator_axis >= 0) {
value /= semi_axes[second_denominator_axis] * semi_axes[second_denominator_axis] + u;
}
return value;
};
double integration_error = 0.0;
return boost::math::quadrature::gauss_kronrod<double, 61>::integrate(
integrand, 0.0, 1.0, 15, 1.0e-13, &integration_error
);
}
static FerrersN1Analytic compute_ferrers_n1_analytic(
const double semi_axis_x,
const double semi_axis_y,
const double semi_axis_z
) {
const std::array<double, 3> semi_axes{semi_axis_x, semi_axis_y, semi_axis_z};
FerrersN1Analytic analytic{
.potential_constant = compute_ferrers_n1_coefficient(semi_axes, -1, -1),
.first_coefficients = {},
.second_coefficients = {}
};
for (int axis = 0; axis < 3; ++axis) {
analytic.first_coefficients[axis] = compute_ferrers_n1_coefficient(semi_axes, axis, -1);
}
for (int first_axis = 0; first_axis < 3; ++first_axis) {
for (int second_axis = first_axis; second_axis < 3; ++second_axis) {
const double coefficient = compute_ferrers_n1_coefficient(semi_axes, first_axis, second_axis);
analytic.second_coefficients[first_axis][second_axis] = coefficient;
analytic.second_coefficients[second_axis][first_axis] = coefficient;
}
}
return analytic;
}
static double evaluate_ferrers_n1_potential(
const mfem::Vector &position,
const double central_density,
const FerrersN1Analytic &analytic
) {
const std::array<double, 3> coordinate_squared{
position(0) * position(0), position(1) * position(1), position(2) * position(2)
};
/*
* Expansion of
*
* -pi G rho_c abc / 2
* integral [(1 - m^2(u))^2 / Delta(u)] du.
*/
double potential_kernel = analytic.potential_constant;
for (int first_axis = 0; first_axis < 3; ++first_axis) {
potential_kernel -= 2.0 * analytic.first_coefficients[first_axis] * coordinate_squared[first_axis];
for (int second_axis = 0; second_axis < 3; ++second_axis) {
potential_kernel += analytic.second_coefficients[first_axis][second_axis] * coordinate_squared[first_axis] *
coordinate_squared[second_axis];
}
}
return -0.5 * M_PI * utils::G * central_density * potential_kernel;
}
static void evaluate_ferrers_n1_gradient(
const mfem::Vector &position,
const double central_density,
const FerrersN1Analytic &analytic,
mfem::Vector &gradient
) {
gradient.SetSize(3);
const std::array<double, 3> coordinate_squared{
position(0) * position(0), position(1) * position(1), position(2) * position(2)
};
for (int axis = 0; axis < 3; ++axis) {
double coefficient = analytic.first_coefficients[axis];
for (int other_axis = 0; other_axis < 3; ++other_axis) {
coefficient -= analytic.second_coefficients[axis][other_axis] * coordinate_squared[other_axis];
}
/*
* The solver stores grad(Phi), which points outward for a
* negative gravitational potential.
*/
gradient(axis) = 2.0 * M_PI * utils::G * central_density * position(axis) * coefficient;
}
}
TEST_CASE(
"Gravity Field Matches Analytic Ferrers Ellipsoid",
tags::gravity_analytic_accuracy
) {
auto args = test_utils::setup_args();
args.p.rtol = 1.0e-13;
args.p.atol = 1.0e-14;
args.p.max_iters = std::max(args.p.max_iters, 2000);
fem::FEM fem = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(fem.domainMapperStateless != nullptr);
REQUIRE(fem.domainMapperStateless != nullptr);
const double radius = utils::RADIUS;
const double mass = utils::MASS;
constexpr double x_scale = 1.0;
constexpr double y_scale = 1.0;
constexpr double z_scale = 1.0 / (x_scale * y_scale);
const double semi_axis_x = x_scale * radius;
const double semi_axis_y = y_scale * radius;
const double semi_axis_z = z_scale * radius;
REQUIRE_THAT(x_scale * y_scale * z_scale, Catch::Matchers::WithinAbs(1.0, 1.0e-14));
auto displacement_function = [](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(fem.displacementFes.get());
displacement.ProjectCoefficient(displacement_coefficient);
*fem.displacement = displacement;
const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute;
/*
* For rho = rho_c (1 - m^2), the exact mass is
*
* M = 8 pi a b c rho_c / 15.
*/
const double central_density = 15.0 * mass / (8.0 * M_PI * semi_axis_x * semi_axis_y * semi_axis_z);
auto density_function = [central_density, semi_axis_x, semi_axis_y, semi_axis_z](const mfem::Vector &position) {
const double ellipsoidal_radius_squared = position(0) * position(0) / (semi_axis_x * semi_axis_x) +
position(1) * position(1) / (semi_axis_y * semi_axis_y) +
position(2) * position(2) / (semi_axis_z * semi_axis_z);
return central_density * std::max(0.0, 1.0 - ellipsoidal_radius_squared);
};
mapping::PhysicalPositionFunctionCoefficient physical_density_coefficient(
*fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate, density_function
);
FerrersVacuumMaskedCoefficient stellar_density_coefficient(physical_density_coefficient, vacuum_attribute);
mfem::GridFunction density(fem.densityFes.get());
density = 0.0;
density.ProjectCoefficient(stellar_density_coefficient);
const double projected_mass = analysis::domain_integrate_grid_function(fem, density, utils::DOMAINS::STELLAR);
REQUIRE(std::isfinite(projected_mass));
REQUIRE(projected_mass > 0.0);
/*
* Keep the projected source at exactly the requested mass. Because
* projection and scaling are linear, this also gives the central
* density appropriate to the represented source.
*/
const double density_scale = mass / projected_mass;
density *= density_scale;
const double represented_central_density = central_density * density_scale;
fem.com = analysis::get_com(fem, density);
fem.Q = physics::compute_quadrupole_moment_tensor(fem, density, fem.com);
const double normalized_quadrupole = fem.Q.FNorm() / (mass * radius * radius);
// REQUIRE(normalized_quadrupole > 1.0e-3);
const FerrersN1Analytic analytic = compute_ferrers_n1_analytic(semi_axis_x, semi_axis_y, semi_axis_z);
/*
* Independent analytic consistency checks.
*
* Sum(A_i) = 2 supplies the constant part of Poisson's
* equation. The B_ij identities supply the -m^2 part.
*/
const double first_coefficient_sum =
analytic.first_coefficients[0] + analytic.first_coefficients[1] + analytic.first_coefficients[2];
REQUIRE_THAT(first_coefficient_sum, Catch::Matchers::WithinAbs(2.0, 1.0e-11));
const std::array<double, 3> semi_axes_squared{
semi_axis_x * semi_axis_x, semi_axis_y * semi_axis_y, semi_axis_z * semi_axis_z
};
for (int axis = 0; axis < 3; ++axis) {
double poisson_coefficient = 3.0 * analytic.second_coefficients[axis][axis];
for (int other_axis = 0; other_axis < 3; ++other_axis) {
if (other_axis != axis) {
poisson_coefficient += analytic.second_coefficients[axis][other_axis];
}
}
REQUIRE_THAT(poisson_coefficient, Catch::Matchers::WithinRel(2.0 / semi_axes_squared[axis], 1.0e-10));
}
const physics::GravitySolution solution = physics::solve_gravity_field(fem, args, density, displacement);
auto analytic_potential_function = [represented_central_density, analytic](const mfem::Vector &position) {
return evaluate_ferrers_n1_potential(position, represented_central_density, analytic);
};
mapping::PhysicalPositionFunctionCoefficient physical_potential_coefficient(
*fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate, analytic_potential_function
);
/*
* This wrapper is required because scalar ProjectCoefficient has no
* attribute overload. It also prevents evaluation of the quartic
* interior formula in the compactified vacuum.
*/
FerrersVacuumMaskedCoefficient stellar_potential_coefficient(physical_potential_coefficient, vacuum_attribute);
mfem::ParGridFunction projected_potential(fem.gravityPotentialFes.get());
projected_potential = 0.0;
projected_potential.ProjectCoefficient(stellar_potential_coefficient);
const int quadrature_order = get_gravity_quadrature_order(fem) + 4;
double local_potential_error_squared = 0.0;
double local_projection_error_squared = 0.0;
double local_solution_projection_gap_squared = 0.0;
double local_potential_norm_squared = 0.0;
double local_projection_norm_squared = 0.0;
double local_field_error_squared = 0.0;
double local_field_norm_squared = 0.0;
double local_maximum_potential_error = 0.0;
mfem::Vector physical_position(3);
mfem::Vector reference_field(3);
mfem::Vector physical_field(3);
mfem::Vector analytic_field(3);
mfem::Vector field_difference(3);
mapping::GridFunctionMappingEvaluator mapping_evaluator(
*fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate
);
for (int element_id = 0; element_id < fem.mesh->GetNE(); ++element_id) {
mfem::ElementTransformation *transformation = fem.mesh->GetElementTransformation(element_id);
if (transformation->Attribute == vacuum_attribute) {
continue;
}
const mfem::IntegrationRule &rule = mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order);
for (int quadrature_point_id = 0; quadrature_point_id < rule.GetNPoints(); ++quadrature_point_id) {
const mfem::IntegrationPoint &point = rule.IntPoint(quadrature_point_id);
transformation->SetIntPoint(&point);
mapping::MappingPointContext mapping_context;
MFEM_VERIFY(
mapping_evaluator.EvaluatePoint(*transformation, point, mapping_context) ==
mapping::MappingStatus::valid,
"Ferrers test encountered an invalid mapping."
);
physical_position = mapping_context.physical_position;
const mfem::DenseMatrix &mapping_jacobian = mapping_context.mapping_jacobian;
const double mapping_determinant = mapping_context.mapping_determinant;
MFEM_VERIFY(
std::isfinite(mapping_determinant) && mapping_determinant > 0.0,
"Ferrers test encountered an invalid mapping determinant."
);
const double expected_potential =
evaluate_ferrers_n1_potential(physical_position, represented_central_density, analytic);
evaluate_ferrers_n1_gradient(physical_position, represented_central_density, analytic, analytic_field);
const double computed_potential = solution.phi.GetValue(element_id, point);
const double projected_potential_value = projected_potential.GetValue(element_id, point);
solution.gradPhi.GetVectorValue(element_id, point, reference_field);
mapping_jacobian.Mult(reference_field, physical_field);
physical_field /= mapping_determinant;
field_difference = physical_field;
field_difference -= analytic_field;
const double weight = point.weight * transformation->Weight() * mapping_determinant;
const double potential_error = computed_potential - expected_potential;
const double projection_error = projected_potential_value - expected_potential;
const double solution_projection_difference = computed_potential - projected_potential_value;
local_potential_error_squared += weight * potential_error * potential_error;
local_projection_error_squared += weight * projection_error * projection_error;
local_solution_projection_gap_squared +=
weight * solution_projection_difference * solution_projection_difference;
local_potential_norm_squared += weight * expected_potential * expected_potential;
local_projection_norm_squared += weight * projected_potential_value * projected_potential_value;
local_field_error_squared += weight * (field_difference * field_difference);
local_field_norm_squared += weight * (analytic_field * analytic_field);
local_maximum_potential_error = std::max(
local_maximum_potential_error,
std::abs(potential_error) /
std::max(std::abs(expected_potential), std::numeric_limits<double>::epsilon())
);
}
}
const std::array<double, 7> local_values{
local_potential_error_squared, local_projection_error_squared, local_solution_projection_gap_squared,
local_potential_norm_squared, local_projection_norm_squared, local_field_error_squared,
local_field_norm_squared
};
std::array<double, 7> global_values{};
MPI_Allreduce(
local_values.data(), global_values.data(), static_cast<int>(local_values.size()), MPI_DOUBLE, MPI_SUM,
fem.densityFes->GetComm()
);
double maximum_potential_error = 0.0;
MPI_Allreduce(
&local_maximum_potential_error, &maximum_potential_error, 1, MPI_DOUBLE, MPI_MAX, fem.densityFes->GetComm()
);
REQUIRE(global_values[3] > 0.0);
REQUIRE(global_values[4] > 0.0);
REQUIRE(global_values[6] > 0.0);
const double potential_relative_error = std::sqrt(global_values[0] / global_values[3]);
const double projection_relative_error = std::sqrt(global_values[1] / global_values[3]);
const double solution_projection_gap = std::sqrt(global_values[2] / global_values[4]);
const double field_relative_error = std::sqrt(global_values[5] / global_values[6]);
INFO("Projected mass before normalization = " << projected_mass);
INFO("Density normalization factor = " << density_scale);
INFO("Normalized quadrupole = " << normalized_quadrupole);
INFO("Ferrers potential L2 relative error = " << potential_relative_error);
INFO("Ferrers potential FE-projection relative error = " << projection_relative_error);
INFO("New-solver / Ferrers-potential projection gap = " << solution_projection_gap);
INFO("Ferrers field L2 relative error = " << field_relative_error);
INFO("Maximum interior pointwise potential relative error = " << maximum_potential_error);
REQUIRE(std::isfinite(potential_relative_error));
REQUIRE(std::isfinite(projection_relative_error));
REQUIRE(std::isfinite(solution_projection_gap));
REQUIRE(std::isfinite(field_relative_error));
REQUIRE(std::isfinite(maximum_potential_error));
/*
* On the regression mesh, the quartic analytic potential and its RT
* gradient have representation floors of about 1.1e-4 and 8.7e-4,
* respectively. The mixed solution remains much closer to the direct
* FE projection, with a solution/projection gap below 1e-5.
*/
CHECK(potential_relative_error < 1.5e-4);
CHECK(projection_relative_error < 1.5e-4);
CHECK(solution_projection_gap < 1.0e-5);
CHECK(field_relative_error < 1.0e-3);
CHECK(maximum_potential_error < 5.0e-4);
}