Files
MeanField/tests/physics/gravity.cpp

3966 lines
143 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 {
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_mapping,
const double density,
const HomogeneousEllipsoidAnalytic &analytic
)
: VectorCoefficient(3),
domain_mapping(domain_mapping),
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 x_physical(3);
mfem::Vector field_physical(3);
mfem::DenseMatrix map_jacobian(3, 3);
mfem::DenseMatrix inverse_map_jacobian(3, 3);
domain_mapping.GetPhysicalPoint(
transformation, integration_point, x_physical
);
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);
domain_mapping.ComputeJacobian(transformation, map_jacobian);
const double map_determinant = map_jacobian.Det();
MFEM_VERIFY(
map_determinant > 0.0,
"Domain mapping has a non-positive Jacobian determinant."
);
mfem::CalcInverse(map_jacobian, inverse_map_jacobian);
inverse_map_jacobian.Mult(field_physical, value);
value *= map_determinant;
}
private:
const mapping::DomainMapper &domain_mapping;
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);
mfem::DenseMatrix map_jacobian(dim, dim);
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);
double weight =
transformation->Weight() * integration_point.weight;
if (f.has_mapping()) {
const double map_determinant = f.mapping->ComputeDetJ(
*transformation, integration_point
);
MFEM_VERIFY(
map_determinant > 0.0,
"Domain mapping has a non-positive Jacobian "
"determinant."
);
weight *= map_determinant;
f.mapping->GetPhysicalPoint(
*transformation, integration_point, x_physical
);
gravity_solution.gradPhi.GetVectorValue(
elem_id, integration_point, grad_phi_element
);
f.mapping->ComputeJacobian(*transformation, map_jacobian);
map_jacobian.Mult(grad_phi_element, grad_phi_physical);
grad_phi_physical /= map_determinant;
} else {
transformation->Transform(integration_point, x_physical);
gravity_solution.gradPhi.GetVectorValue(
elem_id, integration_point, 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
) {
for (int i = 0; i < f.vacuumDensityTdofs.Size(); ++i) {
rho(f.vacuumDensityTdofs[i]) = 0.0;
}
}
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 GravitySolutionComparison {
double relative_gradient_difference;
double relative_potential_difference;
};
template <
typename LeftGravitySolution,
typename RightGravitySolution>
GravitySolutionComparison compare_gravity_solutions(
fem::FEM &f,
const LeftGravitySolution &left_solution,
const RightGravitySolution &right_solution,
const int quadrature_order
) {
const int dimension = f.mesh->Dimension();
double local_gradient_difference_squared = 0.0;
double local_left_gradient_norm_squared = 0.0;
double local_right_gradient_norm_squared = 0.0;
double local_potential_difference_squared = 0.0;
double local_left_potential_norm_squared = 0.0;
double local_right_potential_norm_squared = 0.0;
mfem::Vector left_gradient_element(dimension);
mfem::Vector right_gradient_element(dimension);
mfem::Vector left_gradient_physical(dimension);
mfem::Vector right_gradient_physical(dimension);
mfem::Vector gradient_difference(dimension);
mfem::DenseMatrix map_jacobian(dimension, dimension);
for (int element_id = 0; element_id < f.mesh->GetNE(); ++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(), quadrature_order
);
for (int q = 0; q < integration_rule.GetNPoints(); ++q) {
const mfem::IntegrationPoint &integration_point =
integration_rule.IntPoint(q);
transformation->SetIntPoint(&integration_point);
double weight =
transformation->Weight() * integration_point.weight;
left_solution.gradPhi.GetVectorValue(
element_id, integration_point, left_gradient_element
);
right_solution.gradPhi.GetVectorValue(
element_id, integration_point, right_gradient_element
);
if (f.has_mapping()) {
const double map_determinant = f.mapping->ComputeDetJ(
*transformation, integration_point
);
MFEM_VERIFY(
map_determinant > 0.0,
"Domain mapping has a non-positive Jacobian "
"determinant."
);
weight *= map_determinant;
f.mapping->ComputeJacobian(*transformation, map_jacobian);
map_jacobian.Mult(
left_gradient_element, left_gradient_physical
);
map_jacobian.Mult(
right_gradient_element, right_gradient_physical
);
left_gradient_physical /= map_determinant;
right_gradient_physical /= map_determinant;
} else {
left_gradient_physical = left_gradient_element;
right_gradient_physical = right_gradient_element;
}
gradient_difference = left_gradient_physical;
gradient_difference -= right_gradient_physical;
local_gradient_difference_squared +=
(gradient_difference * gradient_difference) * weight;
local_left_gradient_norm_squared +=
(left_gradient_physical * left_gradient_physical) * weight;
local_right_gradient_norm_squared +=
(right_gradient_physical * right_gradient_physical) *
weight;
const double left_potential =
left_solution.phi.GetValue(element_id, integration_point);
const double right_potential =
right_solution.phi.GetValue(element_id, integration_point);
const double potential_difference =
left_potential - right_potential;
local_potential_difference_squared +=
potential_difference * potential_difference * weight;
local_left_potential_norm_squared +=
left_potential * left_potential * weight;
local_right_potential_norm_squared +=
right_potential * right_potential * weight;
}
}
const std::array<double, 6> local_values{
local_gradient_difference_squared,
local_left_gradient_norm_squared,
local_right_gradient_norm_squared,
local_potential_difference_squared,
local_left_potential_norm_squared,
local_right_potential_norm_squared
};
std::array<double, 6> global_values{};
MPI_Allreduce(
local_values.data(), global_values.data(),
static_cast<int>(local_values.size()), MPI_DOUBLE, MPI_SUM,
f.densityFes->GetComm()
);
const double gradient_scale_squared =
0.5 * (global_values[1] + global_values[2]);
const double potential_scale_squared =
0.5 * (global_values[4] + global_values[5]);
MFEM_VERIFY(
gradient_scale_squared > 0.0,
"Cannot compare gravity solutions with zero gradient norm."
);
MFEM_VERIFY(
potential_scale_squared > 0.0,
"Cannot compare gravity solutions with zero potential norm."
);
return {
.relative_gradient_difference =
std::sqrt(global_values[0] / gradient_scale_squared),
.relative_potential_difference =
std::sqrt(global_values[3] / potential_scale_squared)
};
}
struct GravityResidualMetrics {
double relative_total;
double relative_gradient;
double relative_poisson;
};
double gravity_test_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);
}
void gravity_test_get_true_dofs(
const mfem::ParFiniteElementSpace &finite_element_space,
const mfem::GridFunction &grid_function,
mfem::Vector &true_dofs
) {
MFEM_VERIFY(
grid_function.Size() == finite_element_space.GetVSize(),
"Grid function has the wrong local size."
);
true_dofs.SetSize(finite_element_space.GetTrueVSize());
const mfem::Operator *restriction =
finite_element_space.GetRestrictionMatrix();
if (restriction != nullptr) {
restriction->Mult(grid_function, true_dofs);
} else {
MFEM_VERIFY(
grid_function.Size() == true_dofs.Size(),
"Local and true sizes do not match."
);
true_dofs = grid_function;
}
}
GravityResidualMetrics measure_gravity_residual(
const mfem::Vector &residual,
const mfem::Array<int> &offsets,
const double right_hand_side_norm,
MPI_Comm communicator
) {
MFEM_VERIFY(
offsets.Size() == 3, "Gravity residual must contain two blocks."
);
MFEM_VERIFY(
residual.Size() == offsets.Last(),
"Gravity residual has the wrong size."
);
MFEM_VERIFY(
right_hand_side_norm > 0.0,
"Gravity right-hand side must be nonzero."
);
mfem::Vector gradient_residual(offsets[1] - offsets[0]);
mfem::Vector poisson_residual(offsets[2] - offsets[1]);
for (int i = 0; i < gradient_residual.Size(); ++i) {
gradient_residual(i) = residual(offsets[0] + i);
}
for (int i = 0; i < poisson_residual.Size(); ++i) {
poisson_residual(i) = residual(offsets[1] + i);
}
return {
.relative_total = gravity_test_global_norm(residual, communicator) /
right_hand_side_norm,
.relative_gradient =
gravity_test_global_norm(gradient_residual, communicator) /
right_hand_side_norm,
.relative_poisson =
gravity_test_global_norm(poisson_residual, communicator) /
right_hand_side_norm
};
}
enum class ExteriorMonopoleMapping { legacy, stateless };
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 ExteriorMonopoleMapping mapping_path,
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.mapping != 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::DomainMapperStateless::Workspace workspace(
f.mesh->Dimension()
);
const int vacuum_attribute =
f.domainMapperStateless->GetVacuumElementAttribute();
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
);
if (mapping_path == ExteriorMonopoleMapping::stateless) {
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
);
} else {
/*
* The legacy path intentionally does not use the exterior
* coordinate to construct its physical mapping.
*/
f.mapping->GetPhysicalPoint(
*transformation, integration_point, physical_position
);
mfem::DenseMatrix mapping_jacobian(3);
f.mapping->ComputeJacobian(
*transformation, mapping_jacobian
);
const double mapping_determinant = mapping_jacobian.Det();
REQUIRE(std::isfinite(mapping_determinant));
REQUIRE(mapping_determinant > 0.0);
mapping_jacobian.Mult(reference_field, physical_field);
physical_field /= mapping_determinant;
}
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::DomainMapperStateless &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::DomainMapperStateless &m_domain_mapper;
const mfem::GridFunction &m_displacement;
mapping::DomainMapperStateless::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::DomainMapperStateless &domain_mapper,
const mfem::GridFunction &displacement,
const double mass,
const double stellar_radius
)
: m_geometry(
f,
domain_mapper,
displacement
),
m_vacuum_attribute(domain_mapper.GetVacuumElementAttribute()),
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::DomainMapperStateless &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(domain_mapper.GetVacuumElementAttribute()),
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 &tags::analytic_comparison &tags::initialization
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
f.mapping->ResetDisplacement();
physics::update_stiffness_matrix(f);
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::grav_potential(f, args, rho_uniform);
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;
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;
f.mapping->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 &tags::analytic_comparison &tags::initialization
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
f.mapping->ResetDisplacement();
physics::update_stiffness_matrix(f);
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.mapping, 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::grav_potential(f, args, rho_grid);
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 &tags::self_consistency &tags::initialization
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
f.mapping->ResetDisplacement();
physics::update_stiffness_matrix(f);
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.mapping, 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::grav_potential(f, args, rho_grid);
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);
constexpr double virial_tolerance = 1.0e-5;
CHECK_THAT(
relative_consistency_error,
Catch::Matchers::WithinAbs(0.0, virial_tolerance)
);
}
TEST_CASE(
"Homogeneous Ellipsoid Analytic Gravity",
tags::gravity &tags::analytic_comparison &tags::initialization
) {
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.mapping->SetDisplacement(displacement);
physics::update_stiffness_matrix(f);
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.mapping, 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::grav_potential(f, args, rho_grid);
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::DenseMatrix map_jacobian(3, 3);
mfem::Vector gravity_projection_difference(3);
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);
const double map_determinant =
f.mapping->ComputeDetJ(*transformation, integration_point);
MFEM_VERIFY(
map_determinant > 0.0,
"Domain mapping has a non-positive Jacobian determinant."
);
const double weight = transformation->Weight() *
integration_point.weight * map_determinant;
f.mapping->GetPhysicalPoint(
*transformation, integration_point, x_physical
);
gravity_solution.gradPhi.GetVectorValue(
elem_id, integration_point, grad_phi_element
);
f.mapping->ComputeJacobian(*transformation, map_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));
constexpr double quadrupole_tolerance = 2.0e-4;
constexpr double field_tolerance = 1.0e-5;
constexpr double energy_tolerance = 1.0e-5;
constexpr double consistency_tolerance = 1.0e-5;
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, energy_tolerance)
);
CHECK_THAT(
relative_virial_energy_error,
Catch::Matchers::WithinAbs(0.0, energy_tolerance)
);
CHECK_THAT(
relative_consistency_error,
Catch::Matchers::WithinAbs(0.0, consistency_tolerance)
);
}
TEST_CASE(
"Deformed Rational Density Virial Self-Consistency",
tags::gravity &tags::self_consistency &tags::initialization
) {
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.mapping->SetDisplacement(displacement);
physics::update_stiffness_matrix(f);
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.mapping, 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::grav_potential(f, args, rho_grid);
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);
constexpr double virial_tolerance = 1.0e-5;
CHECK_THAT(
relative_consistency_error,
Catch::Matchers::WithinAbs(0.0, virial_tolerance)
);
}
TEST_CASE(
"New Gravity Potential Matches Uniform Sphere Analytic",
tags::gravity &tags::analytic_comparison &tags::initialization
&tags::integration
) {
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.mapping->ResetDisplacement();
physics::update_stiffness_matrix(f);
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::grav_potential_new(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;
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;
f.mapping->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(
"New Gravity Potential Matches Legacy Solver On Homogeneous Ellipsoid",
tags::gravity &tags::analytic_comparison &tags::initialization
&tags::integration &tags::legacy_comparison
) {
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.mapping->SetDisplacement(displacement);
physics::update_stiffness_matrix(f);
REQUIRE(f.gravityContext.block_A != nullptr);
REQUIRE(f.gravityContext.source_form != nullptr);
REQUIRE(f.domainMapperStateless != nullptr);
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 physics::GravitySolution legacy_solution =
physics::grav_potential(f, args, rho_grid);
const physics::GravitySolution new_solution =
physics::grav_potential_new(f, args, rho_grid, displacement);
using gravity_form = utils::blocks::gravity_field_form;
constexpr auto gravity_gradient_residual_block =
utils::blocks::get_residual_block<gravity_form>(
utils::blocks::gravity_field.gradient_term
);
constexpr auto gravity_poisson_residual_block =
utils::blocks::get_residual_block<gravity_form>(
utils::blocks::gravity_field.poisson_term
);
const std::array<int, gravity_form::value_block_count> value_sizes{
f.densityFes->GetTrueVSize(), f.displacementFes->GetTrueVSize(),
f.gravityFluxFes->GetTrueVSize(), f.gravityPotentialFes->GetTrueVSize()
};
const std::array<int, gravity_form::residual_block_count> residual_sizes{
f.gravityFluxFes->GetTrueVSize(), f.gravityPotentialFes->GetTrueVSize()
};
const utils::blocks::form_layout<gravity_form> gravity_layout(
value_sizes, residual_sizes
);
const int gravity_system_size = gravity_layout.residual_offsets().Last();
const int gravity_gradient_size =
gravity_layout.size(gravity_gradient_residual_block);
const int gravity_poisson_size =
gravity_layout.size(gravity_poisson_residual_block);
mfem::Vector density_true;
mfem::Vector displacement_true;
gravity_test_get_true_dofs(*f.densityFes, rho_grid, density_true);
gravity_test_get_true_dofs(
*f.displacementFes, displacement, displacement_true
);
REQUIRE(density_true.Size() == f.densityFes->GetTrueVSize());
REQUIRE(displacement_true.Size() == f.displacementFes->GetTrueVSize());
operators::context::gravity_field::GravityFieldLinearizationContext
residual_linearization_context(f, *f.domainMapperStateless);
operators::GravityFieldJacobianOperator residual_jacobian(
f, *f.domainMapperStateless, residual_linearization_context,
gravity_layout.value_offsets(), gravity_layout.residual_offsets()
);
operators::GravityFieldOperator residual_gravity_operator(
f, *f.domainMapperStateless, residual_linearization_context,
gravity_layout.value_offsets(), residual_jacobian
);
operators::context::gravity_field::GravityFieldGeometryContext
residual_geometry_context(f, *f.domainMapperStateless);
operators::ReducedGravityFieldOperator new_reduced_operator(
residual_gravity_operator, residual_geometry_context, displacement_true
);
REQUIRE(new_reduced_operator.Width() == gravity_system_size);
REQUIRE(new_reduced_operator.Height() == gravity_system_size);
mfem::Vector new_right_hand_side;
new_reduced_operator.BuildRightHandSide(density_true, new_right_hand_side);
REQUIRE(new_right_hand_side.Size() == gravity_system_size);
REQUIRE(f.gravityContext.source_form->Width() == density_true.Size());
REQUIRE(f.gravityContext.source_form->Height() == gravity_poisson_size);
mfem::Vector legacy_source_action(f.gravityContext.source_form->Height());
legacy_source_action = 0.0;
f.gravityContext.source_form->Mult(density_true, legacy_source_action);
REQUIRE(legacy_source_action.Size() == gravity_poisson_size);
mfem::BlockVector legacy_right_hand_side(gravity_layout.residual_offsets());
legacy_right_hand_side = 0.0;
legacy_right_hand_side.GetBlock(gravity_poisson_residual_block) =
legacy_source_action;
REQUIRE(legacy_right_hand_side.Size() == gravity_system_size);
MPI_Comm communicator = f.densityFes->GetComm();
const double new_right_hand_side_norm =
gravity_test_global_norm(new_right_hand_side, communicator);
const double legacy_right_hand_side_norm =
gravity_test_global_norm(legacy_right_hand_side, communicator);
REQUIRE(new_right_hand_side_norm > 0.0);
REQUIRE(legacy_right_hand_side_norm > 0.0);
mfem::Vector right_hand_side_difference(new_right_hand_side);
right_hand_side_difference -= legacy_right_hand_side;
const double right_hand_side_scale = std::max(
0.5 * (new_right_hand_side_norm + legacy_right_hand_side_norm),
std::numeric_limits<double>::min()
);
const double relative_right_hand_side_difference =
gravity_test_global_norm(right_hand_side_difference, communicator) /
right_hand_side_scale;
mfem::BlockVector legacy_gravity_state(gravity_layout.residual_offsets());
mfem::BlockVector new_gravity_state(gravity_layout.residual_offsets());
legacy_gravity_state = 0.0;
new_gravity_state = 0.0;
{
mfem::Vector gradient_true;
mfem::Vector potential_true;
legacy_solution.gradPhi.GetTrueDofs(gradient_true);
legacy_solution.phi.GetTrueDofs(potential_true);
REQUIRE(gradient_true.Size() == gravity_gradient_size);
REQUIRE(potential_true.Size() == gravity_poisson_size);
legacy_gravity_state.GetBlock(gravity_gradient_residual_block) =
gradient_true;
legacy_gravity_state.GetBlock(gravity_poisson_residual_block) =
potential_true;
new_solution.gradPhi.GetTrueDofs(gradient_true);
new_solution.phi.GetTrueDofs(potential_true);
REQUIRE(gradient_true.Size() == gravity_gradient_size);
REQUIRE(potential_true.Size() == gravity_poisson_size);
new_gravity_state.GetBlock(gravity_gradient_residual_block) =
gradient_true;
new_gravity_state.GetBlock(gravity_poisson_residual_block) =
potential_true;
}
REQUIRE(legacy_gravity_state.Size() == gravity_system_size);
REQUIRE(new_gravity_state.Size() == gravity_system_size);
REQUIRE(f.gravityContext.block_A->Width() == gravity_system_size);
REQUIRE(f.gravityContext.block_A->Height() == gravity_system_size);
mfem::Vector legacy_action_at_legacy_solution(gravity_system_size);
mfem::Vector legacy_action_at_new_solution(gravity_system_size);
mfem::Vector new_action_at_legacy_solution(gravity_system_size);
mfem::Vector new_action_at_new_solution(gravity_system_size);
legacy_action_at_legacy_solution = 0.0;
legacy_action_at_new_solution = 0.0;
new_action_at_legacy_solution = 0.0;
new_action_at_new_solution = 0.0;
f.gravityContext.block_A->Mult(
legacy_gravity_state, legacy_action_at_legacy_solution
);
f.gravityContext.block_A->Mult(
new_gravity_state, legacy_action_at_new_solution
);
new_reduced_operator.Mult(
legacy_gravity_state, new_action_at_legacy_solution
);
new_reduced_operator.Mult(new_gravity_state, new_action_at_new_solution);
REQUIRE(legacy_action_at_legacy_solution.Size() == gravity_system_size);
REQUIRE(legacy_action_at_new_solution.Size() == gravity_system_size);
REQUIRE(new_action_at_legacy_solution.Size() == gravity_system_size);
REQUIRE(new_action_at_new_solution.Size() == gravity_system_size);
mfem::Vector legacy_residual_at_legacy_solution(
legacy_action_at_legacy_solution
);
mfem::Vector legacy_residual_at_new_solution(legacy_action_at_new_solution);
mfem::Vector new_residual_at_legacy_solution(new_action_at_legacy_solution);
mfem::Vector new_residual_at_new_solution(new_action_at_new_solution);
legacy_residual_at_legacy_solution -= legacy_right_hand_side;
legacy_residual_at_new_solution -= legacy_right_hand_side;
new_residual_at_legacy_solution -= new_right_hand_side;
new_residual_at_new_solution -= new_right_hand_side;
const GravityResidualMetrics legacy_at_legacy = measure_gravity_residual(
legacy_residual_at_legacy_solution, gravity_layout.residual_offsets(),
legacy_right_hand_side_norm, communicator
);
const GravityResidualMetrics legacy_at_new = measure_gravity_residual(
legacy_residual_at_new_solution, gravity_layout.residual_offsets(),
legacy_right_hand_side_norm, communicator
);
const GravityResidualMetrics new_at_legacy = measure_gravity_residual(
new_residual_at_legacy_solution, gravity_layout.residual_offsets(),
new_right_hand_side_norm, communicator
);
const GravityResidualMetrics new_at_new = measure_gravity_residual(
new_residual_at_new_solution, gravity_layout.residual_offsets(),
new_right_hand_side_norm, communicator
);
mfem::Vector operator_gap_at_legacy_solution(
new_residual_at_legacy_solution
);
operator_gap_at_legacy_solution -= legacy_residual_at_legacy_solution;
mfem::Vector operator_gap_at_new_solution(new_residual_at_new_solution);
operator_gap_at_new_solution -= legacy_residual_at_new_solution;
const GravityResidualMetrics gap_at_legacy = measure_gravity_residual(
operator_gap_at_legacy_solution, gravity_layout.residual_offsets(),
right_hand_side_scale, communicator
);
const GravityResidualMetrics gap_at_new = measure_gravity_residual(
operator_gap_at_new_solution, gravity_layout.residual_offsets(),
right_hand_side_scale, communicator
);
INFO(
"New/legacy right-hand-side difference = "
<< relative_right_hand_side_difference
);
INFO(
"Legacy operator at legacy solution: total = "
<< legacy_at_legacy.relative_total
<< ", gradient = " << legacy_at_legacy.relative_gradient
<< ", Poisson = " << legacy_at_legacy.relative_poisson
);
INFO(
"Legacy operator at new solution: total = "
<< legacy_at_new.relative_total
<< ", gradient = " << legacy_at_new.relative_gradient
<< ", Poisson = " << legacy_at_new.relative_poisson
);
INFO(
"New operator at legacy solution: total = "
<< new_at_legacy.relative_total
<< ", gradient = " << new_at_legacy.relative_gradient
<< ", Poisson = " << new_at_legacy.relative_poisson
);
INFO(
"New operator at new solution: total = "
<< new_at_new.relative_total
<< ", gradient = " << new_at_new.relative_gradient
<< ", Poisson = " << new_at_new.relative_poisson
);
INFO(
"Operator gap at legacy solution: total = "
<< gap_at_legacy.relative_total
<< ", gradient = " << gap_at_legacy.relative_gradient
<< ", Poisson = " << gap_at_legacy.relative_poisson
);
INFO(
"Operator gap at new solution: total = "
<< gap_at_new.relative_total
<< ", gradient = " << gap_at_new.relative_gradient
<< ", Poisson = " << gap_at_new.relative_poisson
);
REQUIRE(std::isfinite(legacy_at_legacy.relative_total));
REQUIRE(std::isfinite(legacy_at_new.relative_total));
REQUIRE(std::isfinite(new_at_legacy.relative_total));
REQUIRE(std::isfinite(new_at_new.relative_total));
REQUIRE(std::isfinite(gap_at_legacy.relative_total));
REQUIRE(std::isfinite(gap_at_new.relative_total));
constexpr double source_parity_tolerance = 1.0e-12;
constexpr double diagonal_residual_tolerance = 1.0e-8;
constexpr double poisson_gap_tolerance = 1.0e-11;
CHECK_THAT(
relative_right_hand_side_difference,
Catch::Matchers::WithinAbs(0.0, source_parity_tolerance)
);
CHECK_THAT(
legacy_at_legacy.relative_total,
Catch::Matchers::WithinAbs(0.0, diagonal_residual_tolerance)
);
CHECK_THAT(
new_at_new.relative_total,
Catch::Matchers::WithinAbs(0.0, diagonal_residual_tolerance)
);
CHECK_THAT(
gap_at_legacy.relative_poisson,
Catch::Matchers::WithinAbs(0.0, poisson_gap_tolerance)
);
CHECK_THAT(
gap_at_new.relative_poisson,
Catch::Matchers::WithinAbs(0.0, poisson_gap_tolerance)
);
const int quadrature_order = get_gravity_quadrature_order(f);
const GravitySolutionComparison comparison = compare_gravity_solutions(
f, legacy_solution, new_solution, quadrature_order
);
const GravitationalEnergies legacy_energies =
compute_gravitational_energies(
f, rho_grid, legacy_solution, quadrature_order
);
const GravitationalEnergies new_energies = compute_gravitational_energies(
f, rho_grid, new_solution, quadrature_order
);
const HomogeneousEllipsoidAnalytic analytic =
compute_homogeneous_ellipsoid_analytic(
semi_axis_x, semi_axis_y, semi_axis_z
);
const double analytic_binding_energy =
-(3.0 / 10.0) * utils::G * mass * mass * analytic.energy_kernel;
const double new_binding_analytic_error =
std::abs(new_energies.binding - analytic_binding_energy) /
std::abs(analytic_binding_energy);
const double new_virial_analytic_error =
std::abs(new_energies.virial - analytic_binding_energy) /
std::abs(analytic_binding_energy);
const double new_virial_consistency_error =
std::abs(new_energies.binding - new_energies.virial) /
std::abs(new_energies.binding);
const double relative_binding_difference =
std::abs(new_energies.binding - legacy_energies.binding) /
std::max(
0.5 * (std::abs(new_energies.binding) +
std::abs(legacy_energies.binding)),
std::numeric_limits<double>::min()
);
const double relative_virial_difference =
std::abs(new_energies.virial - legacy_energies.virial) /
std::max(
0.5 * (std::abs(new_energies.virial) +
std::abs(legacy_energies.virial)),
std::numeric_limits<double>::min()
);
INFO("Numerical density = " << numerical_density);
INFO("Analytic binding energy = " << analytic_binding_energy);
INFO("Legacy binding energy = " << legacy_energies.binding);
INFO("New binding energy = " << new_energies.binding);
INFO("Legacy virial energy = " << legacy_energies.virial);
INFO("New virial energy = " << new_energies.virial);
INFO("New binding analytic error = " << new_binding_analytic_error);
INFO("New virial analytic error = " << new_virial_analytic_error);
INFO("New virial consistency error = " << new_virial_consistency_error);
INFO(
"New/legacy physical gradient difference = "
<< comparison.relative_gradient_difference
);
INFO(
"New/legacy physical potential difference = "
<< comparison.relative_potential_difference
);
INFO(
"New/legacy binding-energy difference = " << relative_binding_difference
);
INFO(
"New/legacy virial-energy difference = " << relative_virial_difference
);
REQUIRE(legacy_energies.binding < 0.0);
REQUIRE(legacy_energies.virial < 0.0);
REQUIRE(new_energies.binding < 0.0);
REQUIRE(new_energies.virial < 0.0);
REQUIRE(std::isfinite(comparison.relative_gradient_difference));
REQUIRE(std::isfinite(comparison.relative_potential_difference));
constexpr double gradient_parity_tolerance = 5.0e-4;
constexpr double potential_parity_tolerance = 1.0e-4;
constexpr double energy_parity_tolerance = 1.0e-5;
constexpr double analytic_energy_tolerance = 1.0e-5;
constexpr double consistency_tolerance = 1.0e-5;
CHECK_THAT(
comparison.relative_gradient_difference,
Catch::Matchers::WithinAbs(0.0, gradient_parity_tolerance)
);
CHECK_THAT(
comparison.relative_potential_difference,
Catch::Matchers::WithinAbs(0.0, potential_parity_tolerance)
);
CHECK_THAT(
relative_binding_difference,
Catch::Matchers::WithinAbs(0.0, energy_parity_tolerance)
);
CHECK_THAT(
relative_virial_difference,
Catch::Matchers::WithinAbs(0.0, energy_parity_tolerance)
);
CHECK_THAT(
new_binding_analytic_error,
Catch::Matchers::WithinAbs(0.0, analytic_energy_tolerance)
);
CHECK_THAT(
new_virial_analytic_error,
Catch::Matchers::WithinAbs(0.0, analytic_energy_tolerance)
);
CHECK_THAT(
new_virial_consistency_error,
Catch::Matchers::WithinAbs(0.0, consistency_tolerance)
);
}
TEST_CASE(
"New Gravity Potential Deformed Rational Density Virial Self-Consistency",
tags::gravity &tags::self_consistency &tags::initialization
&tags::integration
) {
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.mapping->SetDisplacement(displacement);
physics::update_stiffness_matrix(f);
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.mapping, 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::grav_potential_new(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 virial_tolerance = 1.0e-5;
constexpr double diagnostic_quadrature_tolerance = 1.0e-7;
CHECK_THAT(
fine_consistency_error,
Catch::Matchers::WithinAbs(0.0, 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(
"New Gravity Potential Resolves Exterior Monopole By Compactification "
"Shell",
tags::gravity &tags::analytic_comparison &tags::initialization
&tags::integration
) {
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.mapping != nullptr);
REQUIRE(f.domainMapperStateless != nullptr);
REQUIRE(f.compactificationCoordinate != nullptr);
const double radius = utils::RADIUS;
const double mass = utils::MASS;
const double analytic_volume =
(4.0 / 3.0) * M_PI * radius * radius * radius;
const double density = mass / analytic_volume;
mfem::ParGridFunction displacement(f.displacementFes.get());
displacement = 0.0;
f.mapping->ResetDisplacement();
physics::update_stiffness_matrix(f);
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 legacy_solution =
physics::grav_potential(f, args, rho_uniform);
const physics::GravitySolution new_solution =
physics::grav_potential_new(f, args, rho_uniform, displacement);
const std::array<ExteriorMonopoleShellMetrics, 5> legacy_metrics =
measure_exterior_monopole_shells(
f, legacy_solution, displacement, ExteriorMonopoleMapping::legacy,
mass
);
const std::array<ExteriorMonopoleShellMetrics, 5> new_metrics =
measure_exterior_monopole_shells(
f, new_solution, displacement, ExteriorMonopoleMapping::stateless,
mass
);
double maximum_new_potential_error = 0.0;
double maximum_new_radial_field_error = 0.0;
double maximum_new_tangential_field = 0.0;
for (int shell = 0; shell < 5; ++shell) {
DYNAMIC_SECTION(
"Exterior coordinate in ["
<< exterior_shell_boundaries[shell] << ", "
<< exterior_shell_boundaries[shell + 1] << ")"
) {
const ExteriorMonopoleShellMetrics &legacy = legacy_metrics[shell];
const ExteriorMonopoleShellMetrics &current = new_metrics[shell];
maximum_new_potential_error = std::max(
maximum_new_potential_error, current.potential_rms_error
);
maximum_new_radial_field_error = std::max(
maximum_new_radial_field_error, current.radial_field_rms_error
);
maximum_new_tangential_field = std::max(
maximum_new_tangential_field, current.tangential_field_rms
);
INFO("Shell = " << shell);
INFO(
"Exterior-coordinate interval = ["
<< exterior_shell_boundaries[shell] << ", "
<< exterior_shell_boundaries[shell + 1] << ")"
);
INFO(
"Legacy physical-radius range = ["
<< legacy.minimum_radius << ", " << legacy.maximum_radius << "]"
);
INFO(
"New physical-radius range = ["
<< current.minimum_radius << ", " << current.maximum_radius
<< "]"
);
INFO(
"Legacy scaled-potential RMS error = "
<< legacy.potential_rms_error
);
INFO(
"New scaled-potential RMS error = "
<< current.potential_rms_error
);
INFO(
"Legacy scaled-radial-field RMS error = "
<< legacy.radial_field_rms_error
);
INFO(
"New scaled-radial-field RMS error = "
<< current.radial_field_rms_error
);
INFO(
"Legacy scaled-tangential-field RMS = "
<< legacy.tangential_field_rms
);
INFO(
"New scaled-tangential-field RMS = "
<< current.tangential_field_rms
);
REQUIRE(legacy.quadrature_points > 0);
REQUIRE(current.quadrature_points > 0);
REQUIRE(std::isfinite(current.minimum_radius));
REQUIRE(std::isfinite(current.maximum_radius));
REQUIRE(std::isfinite(current.potential_rms_error));
REQUIRE(std::isfinite(current.radial_field_rms_error));
REQUIRE(std::isfinite(current.tangential_field_rms));
CHECK(current.minimum_radius > 0.0);
CHECK(current.maximum_radius > current.minimum_radius);
/*
* These are broad regression bounds, not the virial target.
* The measured values will determine whether a monopole lift
* is warranted.
*/
CHECK(current.potential_rms_error < 1.0e-2);
CHECK(current.radial_field_rms_error < 1.0e-2);
CHECK(current.tangential_field_rms < 1.0e-2);
}
}
for (int shell = 1; shell < 5; ++shell) {
CHECK(
new_metrics[shell].minimum_radius >=
new_metrics[shell - 1].minimum_radius
);
CHECK(
new_metrics[shell].maximum_radius >
new_metrics[shell - 1].maximum_radius
);
}
INFO(
"Maximum new scaled-potential shell error = "
<< maximum_new_potential_error
);
INFO(
"Maximum new scaled-radial-field shell error = "
<< maximum_new_radial_field_error
);
INFO(
"Maximum new scaled-tangential-field shell amplitude = "
<< maximum_new_tangential_field
);
}
TEST_CASE(
"New Exterior Monopole Error Is Separated From Finite Element Projection "
"Floor",
tags::gravity &tags::analytic_comparison &tags::initialization
&tags::integration &tags::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.mapping != 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.mapping->ResetDisplacement();
physics::update_stiffness_matrix(f);
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::grav_potential_new(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,
ExteriorMonopoleMapping::stateless, mass
);
const std::array<ExteriorMonopoleShellMetrics, 5> projection_metrics =
measure_exterior_monopole_shells(
f, analytic_projection, displacement,
ExteriorMonopoleMapping::stateless, 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(
"New Gravity Potential Matches Analytic Interior Potential For A Deformed "
"Homogeneous Star",
tags::gravity &tags::analytic_comparison &tags::initialization
&tags::integration &tags::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.mapping != 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.mapping->SetDisplacement(displacement);
physics::update_stiffness_matrix(fem);
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.mapping, analytic_potential
);
const int vacuum_attribute =
fem.domainMapperStateless->GetVacuumElementAttribute();
mfem::ParGridFunction projected_potential(fem.gravityPotentialFes.get());
projected_potential.ProjectCoefficient(analytic_potential_coefficient);
const physics::GravitySolution solution =
physics::grav_potential_new(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);
mfem::DenseMatrix mapping_jacobian(3);
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);
fem.mapping->GetPhysicalPoint(
*transformation, point, physical_position
);
fem.mapping->ComputeJacobian(*transformation, mapping_jacobian);
const double mapping_determinant = mapping_jacobian.Det();
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));
/*
* The field test establishes an O(1e-3) representation floor on this
* mesh. These are intentionally accuracy-regression guards, not claims
* of analytic convergence of the unrefined RT/L2 representation.
*/
CHECK(solution_relative_error < 1.0e-5);
CHECK(projection_relative_error < 1.0e-5);
CHECK(solution_projection_gap < 1.0e-5);
CHECK(maximum_relative_error < 1.0e-5);
}
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(
"New Gravity Potential Matches Analytic Ferrers Ellipsoid",
tags::gravity &tags::analytic_comparison &tags::initialization
&tags::integration &tags::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.mapping != 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.mapping->SetDisplacement(displacement);
physics::update_stiffness_matrix(fem);
const int vacuum_attribute =
fem.domainMapperStateless->GetVacuumElementAttribute();
/*
* 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.mapping, 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::grav_potential_new(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.mapping, 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);
mfem::DenseMatrix mapping_jacobian(3, 3);
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);
fem.mapping->GetPhysicalPoint(
*transformation, point, physical_position
);
fem.mapping->ComputeJacobian(*transformation, mapping_jacobian);
const double mapping_determinant = mapping_jacobian.Det();
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));
/*
* Initial characterization guards. Unlike the homogeneous case,
* this exact potential is quartic, so the FE representation floor
* will generally be higher. Record the values before deciding
* whether tighter regression thresholds are appropriate.
*/
CHECK(potential_relative_error < 1.0e-5);
CHECK(projection_relative_error < 1.0e-5);
CHECK(solution_projection_gap < 1.0e-5);
CHECK(field_relative_error < 1.0e-5);
CHECK(maximum_potential_error < 1.0e-5);
}