feat(mean_field): added initial implementation
note this implementation lacks many tests
This commit is contained in:
25
tests/geometry/volume.cpp
Normal file
25
tests/geometry/volume.cpp
Normal file
@@ -0,0 +1,25 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
#include <catch2/benchmark/catch_benchmark.hpp>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <mfem.hpp>
|
||||
#include <mpi.h>
|
||||
|
||||
import mean_field;
|
||||
import test_helpers;
|
||||
|
||||
using namespace mean_field;
|
||||
|
||||
|
||||
TEST_CASE("STROID Volume vs sphere", tags::geometry & tags::volume) {
|
||||
auto args = test_utils::setup_args();
|
||||
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
|
||||
const double analytic_vol = (4.0 / 3.0) * M_PI * std::pow(utils::RADIUS, 3);
|
||||
const double stroid_vol = analysis::get_mesh_volume(f);
|
||||
|
||||
double s = analytic_vol / stroid_vol;
|
||||
CHECK_THAT(stroid_vol, Catch::Matchers::WithinRel(analytic_vol, 1e-6));
|
||||
}
|
||||
600
tests/physics/gravity.cpp
Normal file
600
tests/physics/gravity.cpp
Normal file
@@ -0,0 +1,600 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
#include <cassert>
|
||||
|
||||
#include <boost/math/quadrature/gauss_kronrod.hpp>
|
||||
#include <mfem.hpp>
|
||||
#include <mpi.h>
|
||||
|
||||
import mean_field;
|
||||
import test_helpers;
|
||||
|
||||
using namespace mean_field;
|
||||
|
||||
namespace {
|
||||
struct GravitationalEnergies {
|
||||
double binding;
|
||||
double virial;
|
||||
};
|
||||
|
||||
struct HomogeneousEllipsoidAnalytic {
|
||||
double coefficient_x;
|
||||
double coefficient_y;
|
||||
double coefficient_z;
|
||||
double energy_kernel;
|
||||
};
|
||||
|
||||
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.L2_fes->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.vacuum_tdof_rho.Size(); ++i) {
|
||||
rho(f.vacuum_tdof_rho[i]) = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
int get_gravity_quadrature_order(const fem::FEM& f) {
|
||||
return 2 * std::max(f.L2_fes->GetMaxElementOrder(), f.RT_fes->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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Uniform Potential Matches Analytic", tags::gravity & tags::analytic_comparison) {
|
||||
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.L2_fes.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.L2_fes->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) {
|
||||
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.L2_fes.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) {
|
||||
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.L2_fes.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) {
|
||||
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.Vec_H1_fes.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.L2_fes.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);
|
||||
|
||||
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;
|
||||
|
||||
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::DenseMatrix map_jacobian(3, 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;
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
MPI_Comm communicator = f.L2_fes->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);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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) {
|
||||
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.Vec_H1_fes.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.L2_fes.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));
|
||||
}
|
||||
331
tests/quadrature/policy.cpp
Normal file
331
tests/quadrature/policy.cpp
Normal file
@@ -0,0 +1,331 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
#include <mfem.hpp>
|
||||
|
||||
import mean_field;
|
||||
import test_helpers;
|
||||
|
||||
using namespace mean_field;
|
||||
|
||||
namespace {
|
||||
std::string_view get_term_name(const quadrature::Term term) {
|
||||
switch (term) {
|
||||
case quadrature::Term::gravity_hdiv_mass: return "gravity_hdiv_mass";
|
||||
case quadrature::Term::gravity_divergence: return "gravity_divergence";
|
||||
case quadrature::Term::gravity_source: return "gravity_source";
|
||||
case quadrature::Term::gravity_boundary: return "gravity_boundary";
|
||||
case quadrature::Term::density_projection: return "density_projection";
|
||||
case quadrature::Term::mass_conservation: return "mass_conservation";
|
||||
case quadrature::Term::center_of_mass: return "center_of_mass";
|
||||
case quadrature::Term::quadrupole: return "quadrupole";
|
||||
case quadrature::Term::gravitational_energy: return "gravitational_energy";
|
||||
case quadrature::Term::virial: return "virial";
|
||||
case quadrature::Term::error_norm: return "error_norm";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
quadrature::Query make_query(const quadrature::Term term, const int base_order) {
|
||||
return {.term = term, .base_order = base_order};
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Quadrature Policy Computes Base Orders", tags::unit & tags::quadrature) {
|
||||
const quadrature::Policy policy(quadrature::make_rule_set(quadrature::Mode::production));
|
||||
|
||||
quadrature::Query generic_query = {
|
||||
.term = quadrature::Term::gravitational_energy,
|
||||
.trial_order = 3,
|
||||
.test_order = 4,
|
||||
.coefficient_order = 2,
|
||||
.geometry_weight_order = 5
|
||||
};
|
||||
|
||||
const quadrature::Resolution generic_resolution = policy.resolve(generic_query);
|
||||
CHECK(generic_resolution.base_order == 14);
|
||||
CHECK(generic_resolution.boost == 0);
|
||||
CHECK(generic_resolution.order == 14);
|
||||
CHECK_FALSE(generic_resolution.used_fixed_order);
|
||||
|
||||
quadrature::Query divergence_query = {
|
||||
.term = quadrature::Term::gravity_divergence,
|
||||
.trial_order = 3,
|
||||
.test_order = 2,
|
||||
.coefficient_order = 1,
|
||||
.geometry_weight_order = 4
|
||||
};
|
||||
|
||||
CHECK(policy.resolve(divergence_query).base_order == 9);
|
||||
|
||||
divergence_query.trial_order = 0;
|
||||
CHECK(policy.resolve(divergence_query).base_order == 7);
|
||||
|
||||
quadrature::Query explicit_query = {
|
||||
.term = quadrature::Term::gravity_hdiv_mass,
|
||||
.trial_order = 20,
|
||||
.test_order = 20,
|
||||
.coefficient_order = 20,
|
||||
.geometry_weight_order = 20,
|
||||
.base_order = 11
|
||||
};
|
||||
|
||||
CHECK(policy.resolve(explicit_query).base_order == 11);
|
||||
CHECK(policy.resolve(explicit_query).order == 11);
|
||||
}
|
||||
|
||||
TEST_CASE("Quadrature Policy Composes Global and Term Boosts", tags::unit & tags::quadrature) {
|
||||
quadrature::RuleSet rule_set = quadrature::make_rule_set(quadrature::Mode::production, 3);
|
||||
rule_set.gravity_hdiv_mass.boost = 5;
|
||||
rule_set.error_norm.boost = 2;
|
||||
const quadrature::Policy policy(rule_set);
|
||||
|
||||
const quadrature::Resolution mass_resolution = policy.resolve(make_query(quadrature::Term::gravity_hdiv_mass, 7));
|
||||
CHECK(mass_resolution.base_order == 7);
|
||||
CHECK(mass_resolution.boost == 8);
|
||||
CHECK(mass_resolution.order == 15);
|
||||
CHECK_FALSE(mass_resolution.used_fixed_order);
|
||||
|
||||
const quadrature::Resolution error_resolution = policy.resolve(make_query(quadrature::Term::error_norm, 7));
|
||||
CHECK(error_resolution.boost == 5);
|
||||
CHECK(error_resolution.order == 12);
|
||||
|
||||
const quadrature::Resolution source_resolution = policy.resolve(make_query(quadrature::Term::gravity_source, 7));
|
||||
CHECK(source_resolution.boost == 3);
|
||||
CHECK(source_resolution.order == 10);
|
||||
}
|
||||
|
||||
TEST_CASE("Quadrature Fixed Orders Have Defined Precedence", tags::unit & tags::quadrature) {
|
||||
quadrature::RuleSet rule_set = quadrature::make_rule_set(quadrature::Mode::production, 4);
|
||||
rule_set.fallback.fixed_order = 17;
|
||||
rule_set.gravity_hdiv_mass.fixed_order = 23;
|
||||
rule_set.gravity_hdiv_mass.boost = 100;
|
||||
rule_set.gravity_source.boost = 100;
|
||||
const quadrature::Policy policy(rule_set);
|
||||
|
||||
const quadrature::Resolution term_resolution = policy.resolve(make_query(quadrature::Term::gravity_hdiv_mass, 8));
|
||||
CHECK(term_resolution.base_order == 8);
|
||||
CHECK(term_resolution.boost == 0);
|
||||
CHECK(term_resolution.order == 23);
|
||||
CHECK(term_resolution.used_fixed_order);
|
||||
|
||||
const quadrature::Resolution fallback_resolution = policy.resolve(make_query(quadrature::Term::gravity_source, 8));
|
||||
CHECK(fallback_resolution.base_order == 8);
|
||||
CHECK(fallback_resolution.boost == 0);
|
||||
CHECK(fallback_resolution.order == 17);
|
||||
CHECK(fallback_resolution.used_fixed_order);
|
||||
}
|
||||
|
||||
TEST_CASE("Quadrature Modes Apply Their Expected Baseline Boosts", tags::unit & tags::quadrature) {
|
||||
constexpr int base_order = 6;
|
||||
constexpr int global_boost = 3;
|
||||
|
||||
for (const quadrature::Mode mode : {quadrature::Mode::fast, quadrature::Mode::production, quadrature::Mode::convergence}) {
|
||||
const quadrature::Policy policy(quadrature::make_rule_set(mode, global_boost));
|
||||
const quadrature::Resolution resolution = policy.resolve(make_query(quadrature::Term::error_norm, base_order));
|
||||
CHECK(resolution.boost == global_boost);
|
||||
CHECK(resolution.order == base_order + global_boost);
|
||||
}
|
||||
|
||||
const quadrature::Policy reference_policy(quadrature::make_rule_set(quadrature::Mode::reference, global_boost));
|
||||
const quadrature::Resolution reference_resolution = reference_policy.resolve(make_query(quadrature::Term::error_norm, base_order));
|
||||
CHECK(reference_resolution.boost == global_boost + 8);
|
||||
CHECK(reference_resolution.order == base_order + global_boost + 8);
|
||||
}
|
||||
|
||||
TEST_CASE("Quadrature Policy Routes Every Term to Its Control", tags::unit & tags::quadrature) {
|
||||
quadrature::RuleSet rule_set;
|
||||
rule_set.gravity_hdiv_mass.boost = 1;
|
||||
rule_set.gravity_divergence.boost = 2;
|
||||
rule_set.gravity_source.boost = 3;
|
||||
rule_set.gravity_boundary.boost = 4;
|
||||
rule_set.density_projection.boost = 5;
|
||||
rule_set.mass_conservation.boost = 6;
|
||||
rule_set.center_of_mass.boost = 7;
|
||||
rule_set.quadrupole.boost = 8;
|
||||
rule_set.gravitational_energy.boost = 9;
|
||||
rule_set.virial.boost = 10;
|
||||
rule_set.error_norm.boost = 11;
|
||||
const quadrature::Policy policy(rule_set);
|
||||
|
||||
const std::array<std::pair<quadrature::Term, int>, 11> cases = {{
|
||||
{quadrature::Term::gravity_hdiv_mass, 1},
|
||||
{quadrature::Term::gravity_divergence, 2},
|
||||
{quadrature::Term::gravity_source, 3},
|
||||
{quadrature::Term::gravity_boundary, 4},
|
||||
{quadrature::Term::density_projection, 5},
|
||||
{quadrature::Term::mass_conservation, 6},
|
||||
{quadrature::Term::center_of_mass, 7},
|
||||
{quadrature::Term::quadrupole, 8},
|
||||
{quadrature::Term::gravitational_energy, 9},
|
||||
{quadrature::Term::virial, 10},
|
||||
{quadrature::Term::error_norm, 11}
|
||||
}};
|
||||
|
||||
for (const auto& [term, expected_boost] : cases) {
|
||||
DYNAMIC_SECTION(get_term_name(term)) {
|
||||
const quadrature::Resolution resolution = policy.resolve(make_query(term, 20));
|
||||
CHECK(resolution.boost == expected_boost);
|
||||
CHECK(resolution.order == 20 + expected_boost);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Quadrature Policy Rejects Invalid Orders", tags::unit & tags::quadrature) {
|
||||
const quadrature::Policy policy(quadrature::make_rule_set(quadrature::Mode::production));
|
||||
|
||||
quadrature::Query negative_component_query = {
|
||||
.term = quadrature::Term::error_norm,
|
||||
.trial_order = -1
|
||||
};
|
||||
REQUIRE_THROWS_AS(policy.resolve(negative_component_query), std::invalid_argument);
|
||||
|
||||
quadrature::Query negative_base_query = {
|
||||
.term = quadrature::Term::error_norm,
|
||||
.base_order = -1
|
||||
};
|
||||
REQUIRE_THROWS_AS(policy.resolve(negative_base_query), std::invalid_argument);
|
||||
|
||||
quadrature::RuleSet negative_fixed_rule_set;
|
||||
negative_fixed_rule_set.error_norm.fixed_order = -1;
|
||||
const quadrature::Policy negative_fixed_policy(negative_fixed_rule_set);
|
||||
REQUIRE_THROWS_AS(negative_fixed_policy.resolve(make_query(quadrature::Term::error_norm, 3)), std::invalid_argument);
|
||||
|
||||
quadrature::RuleSet negative_resolved_rule_set;
|
||||
negative_resolved_rule_set.fallback.boost = -4;
|
||||
const quadrature::Policy negative_resolved_policy(negative_resolved_rule_set);
|
||||
REQUIRE_THROWS_AS(negative_resolved_policy.resolve(make_query(quadrature::Term::error_norm, 3)), std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST_CASE("MFEM Rule Factory Returns the Resolved Rule", tags::unit & tags::quadrature) {
|
||||
quadrature::RuleSet rule_set = quadrature::make_rule_set(quadrature::Mode::production, 2);
|
||||
rule_set.error_norm.boost = 3;
|
||||
const quadrature::RuleFactory factory{quadrature::Policy(rule_set)};
|
||||
const quadrature::MfemRule selected_rule = factory.get(make_query(quadrature::Term::error_norm, 4), mfem::Geometry::CUBE);
|
||||
const mfem::IntegrationRule& expected_rule = mfem::IntRules.Get(mfem::Geometry::CUBE, 9);
|
||||
|
||||
REQUIRE(selected_rule.integration_rule != nullptr);
|
||||
CHECK(selected_rule.resolution.base_order == 4);
|
||||
CHECK(selected_rule.resolution.boost == 5);
|
||||
CHECK(selected_rule.resolution.order == 9);
|
||||
CHECK(selected_rule.integration_rule == &expected_rule);
|
||||
CHECK(selected_rule.integration_rule->GetNPoints() > 0);
|
||||
}
|
||||
|
||||
TEST_CASE("MFEM Quadrature Rule Integrates Tensor Polynomial Exactly", tags::unit & tags::quadrature) {
|
||||
constexpr int polynomial_degree = 7;
|
||||
const quadrature::RuleFactory factory{quadrature::Policy(quadrature::make_rule_set(quadrature::Mode::production))};
|
||||
const quadrature::MfemRule selected_rule = factory.get(make_query(quadrature::Term::error_norm, polynomial_degree), mfem::Geometry::CUBE);
|
||||
double numerical_integral = 0.0;
|
||||
|
||||
for (int i = 0; i < selected_rule.integration_rule->GetNPoints(); ++i) {
|
||||
const mfem::IntegrationPoint& integration_point = selected_rule.integration_rule->IntPoint(i);
|
||||
numerical_integral += integration_point.weight * std::pow(integration_point.x, polynomial_degree) * std::pow(integration_point.y, polynomial_degree) * std::pow(integration_point.z, polynomial_degree);
|
||||
}
|
||||
|
||||
const double one_dimensional_integral = 1.0 / static_cast<double>(polynomial_degree + 1);
|
||||
const double analytic_integral = one_dimensional_integral * one_dimensional_integral * one_dimensional_integral;
|
||||
CHECK_THAT(numerical_integral, Catch::Matchers::WithinAbs(analytic_integral, 5.0e-14));
|
||||
}
|
||||
|
||||
TEST_CASE("Policy Controlled Hdiv Mass Assembly Matches Overintegrated Reference", tags::quadrature & tags::solver & tags::integration) {
|
||||
mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D(1, 1, 1, mfem::Element::HEXAHEDRON, 1.0, 1.0, 1.0);
|
||||
mfem::RT_FECollection rt_collection(2, 3);
|
||||
mfem::FiniteElementSpace rt_space(&mesh, &rt_collection);
|
||||
const mfem::FiniteElement* rt_element = rt_space.GetTypicalFE();
|
||||
mfem::ElementTransformation* transformation = mesh.GetElementTransformation(0);
|
||||
const int base_order = 2 * rt_element->GetOrder() + transformation->OrderW();
|
||||
|
||||
const quadrature::RuleFactory production_factory{quadrature::Policy(quadrature::make_rule_set(quadrature::Mode::production))};
|
||||
const quadrature::MfemRule production_rule = production_factory.get(make_query(quadrature::Term::gravity_hdiv_mass, base_order), rt_element->GetGeomType());
|
||||
|
||||
quadrature::RuleSet reference_rule_set = quadrature::make_rule_set(quadrature::Mode::production);
|
||||
reference_rule_set.gravity_hdiv_mass.boost = 8;
|
||||
const quadrature::RuleFactory reference_factory{quadrature::Policy(reference_rule_set)};
|
||||
const quadrature::MfemRule reference_rule = reference_factory.get(make_query(quadrature::Term::gravity_hdiv_mass, base_order), rt_element->GetGeomType());
|
||||
|
||||
mfem::BilinearForm production_mass(&rt_space);
|
||||
auto* production_integrator = new mfem::VectorFEMassIntegrator();
|
||||
production_integrator->SetIntegrationRule(*production_rule.integration_rule);
|
||||
production_mass.AddDomainIntegrator(production_integrator);
|
||||
production_mass.Assemble();
|
||||
production_mass.Finalize();
|
||||
|
||||
mfem::BilinearForm reference_mass(&rt_space);
|
||||
auto* reference_integrator = new mfem::VectorFEMassIntegrator();
|
||||
reference_integrator->SetIntegrationRule(*reference_rule.integration_rule);
|
||||
reference_mass.AddDomainIntegrator(reference_integrator);
|
||||
reference_mass.Assemble();
|
||||
reference_mass.Finalize();
|
||||
|
||||
mfem::Vector input(rt_space.GetVSize());
|
||||
mfem::Vector production_output(rt_space.GetVSize());
|
||||
mfem::Vector reference_output(rt_space.GetVSize());
|
||||
for (int i = 0; i < input.Size(); ++i) {
|
||||
input(i) = std::sin(0.37 * static_cast<double>(i + 1));
|
||||
}
|
||||
|
||||
production_mass.Mult(input, production_output);
|
||||
reference_mass.Mult(input, reference_output);
|
||||
|
||||
mfem::Vector difference(production_output);
|
||||
difference -= reference_output;
|
||||
const double relative_difference = difference.Norml2() / reference_output.Norml2();
|
||||
INFO("Production quadrature order = " << production_rule.resolution.order);
|
||||
INFO("Reference quadrature order = " << reference_rule.resolution.order);
|
||||
INFO("Relative operator difference = " << relative_difference);
|
||||
CHECK_THAT(relative_difference, Catch::Matchers::WithinAbs(0.0, 1.0e-12));
|
||||
}
|
||||
|
||||
TEST_CASE("HDiv Mass Helper Resolves the MFEM Baseline", tags::unit & tags::quadrature & tags::solver) {
|
||||
mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D(1, 1, 1, mfem::Element::HEXAHEDRON);
|
||||
mfem::RT_FECollection rt_collection(2, 3);
|
||||
mfem::FiniteElementSpace rt_space(&mesh, &rt_collection);
|
||||
|
||||
quadrature::RuleSet rule_set = quadrature::make_rule_set(quadrature::Mode::production);
|
||||
rule_set.gravity_hdiv_mass.boost = 3;
|
||||
quadrature::RuleFactory factory{quadrature::Policy(std::move(rule_set))};
|
||||
|
||||
const mfem::FiniteElement& element = *rt_space.GetTypicalFE();
|
||||
const mfem::ElementTransformation& transformation = *mesh.GetElementTransformation(0);
|
||||
mfem::VectorFEMassIntegrator integrator;
|
||||
|
||||
const quadrature::Resolution resolution = factory.configure_gravity_hdiv_mass(integrator, quadrature::QuadratureRole::discretization, element, transformation);
|
||||
const int expected_base_order = 2 * element.GetOrder() + transformation.OrderW();
|
||||
|
||||
CHECK(resolution.base_order == expected_base_order);
|
||||
CHECK(resolution.boost == 3);
|
||||
CHECK(resolution.order == expected_base_order + 3);
|
||||
}
|
||||
|
||||
TEST_CASE("Gravity Divergence Helper Resolves Preconditioner Rule", tags::unit & tags::quadrature & tags::solver) {
|
||||
mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D(1, 1, 1, mfem::Element::HEXAHEDRON);
|
||||
mfem::RT_FECollection rt_collection(2, 3);
|
||||
mfem::L2_FECollection l2_collection(2, 3);
|
||||
mfem::FiniteElementSpace rt_space(&mesh, &rt_collection);
|
||||
mfem::FiniteElementSpace l2_space(&mesh, &l2_collection);
|
||||
|
||||
quadrature::RuleSet rule_set = quadrature::make_rule_set(quadrature::Mode::production);
|
||||
rule_set.gravity_divergence.boost = 2;
|
||||
rule_set.roles.preconditioner.boost = 3;
|
||||
quadrature::RuleFactory factory{quadrature::Policy(std::move(rule_set))};
|
||||
|
||||
const mfem::FiniteElement& trial_element = *rt_space.GetTypicalFE();
|
||||
const mfem::FiniteElement& test_element = *l2_space.GetTypicalFE();
|
||||
const mfem::ElementTransformation& transformation = *mesh.GetElementTransformation(0);
|
||||
mfem::VectorFEDivergenceIntegrator integrator;
|
||||
|
||||
const quadrature::Resolution resolution = factory.configure_gravity_divergence(integrator, quadrature::QuadratureRole::preconditioner, trial_element, test_element, transformation);
|
||||
const int expected_base_order = std::max(0, trial_element.GetOrder() - 1) + test_element.GetOrder() + transformation.OrderW();
|
||||
|
||||
CHECK(resolution.base_order == expected_base_order);
|
||||
CHECK(resolution.boost == 5);
|
||||
CHECK(resolution.order == expected_base_order + 5);
|
||||
}
|
||||
97
tests/test_helpers.cppm
Normal file
97
tests/test_helpers.cppm
Normal file
@@ -0,0 +1,97 @@
|
||||
module;
|
||||
#include <string>
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
#include <catch2/internal/catch_stringref.hpp>
|
||||
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
export module test_helpers;
|
||||
import mean_field;
|
||||
|
||||
template <std::size_t N>
|
||||
struct Tag {
|
||||
std::array<char, N> chars{};
|
||||
|
||||
// ReSharper disable once CppNonExplicitConvertingConstructor
|
||||
consteval Tag(std::array<char, N> arr) : chars(arr) {}
|
||||
|
||||
// ReSharper disable once CppNonExplicitConversionOperator
|
||||
constexpr operator const char*() const { return chars.data(); }
|
||||
|
||||
// ReSharper disable once CppNonExplicitConversionOperator
|
||||
constexpr operator Catch::StringRef() const {
|
||||
return Catch::StringRef(chars.data(), N - 1);
|
||||
}
|
||||
|
||||
template <std::size_t M>
|
||||
consteval Tag<N + M - 1> operator&(const Tag<M>& other) const {
|
||||
std::array<char, N + M - 1> res{};
|
||||
std::ranges::copy(chars.begin(), chars.end() - 1, res.begin());
|
||||
std::ranges::copy(other.chars, res.begin() + (N - 1));
|
||||
return {res};
|
||||
}
|
||||
};
|
||||
|
||||
template <std::size_t N>
|
||||
consteval auto make_tag(const char (&str)[N]) {
|
||||
std::array<char, N + 2> res{};
|
||||
res[0] = '[';
|
||||
std::ranges::copy(str, str + N - 1, res.begin() + 1);
|
||||
res[N] = ']';
|
||||
res[N + 1] = '\0';
|
||||
return Tag<N + 2>{res};
|
||||
}
|
||||
|
||||
template <std::size_t N, std::size_t M>
|
||||
consteval auto sub_tag(const Tag<N>& parent, const char (&str)[M]) {
|
||||
return parent & make_tag(str);
|
||||
}
|
||||
|
||||
namespace test_utils::detail {
|
||||
std::optional<mean_field::utils::Args> configured_args;
|
||||
|
||||
mean_field::utils::Args make_default_args() {
|
||||
mean_field::utils::Args args;
|
||||
args.mesh_file = "sandbox.smesh";
|
||||
args.p.rtol = 1.0e-12;
|
||||
args.p.atol = 1.0e-12;
|
||||
return args;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace test_utils {
|
||||
void set_args(mean_field::utils::Args args) {
|
||||
detail::configured_args = std::move(args);
|
||||
}
|
||||
|
||||
mean_field::utils::Args setup_args() {
|
||||
if (detail::configured_args.has_value()) {
|
||||
return *detail::configured_args;
|
||||
}
|
||||
|
||||
return detail::make_default_args();
|
||||
}
|
||||
}
|
||||
|
||||
export namespace tags {
|
||||
inline constexpr auto geometry = make_tag("geometry");
|
||||
inline constexpr auto physics = make_tag("physics");
|
||||
inline constexpr auto unit = make_tag("unit");
|
||||
inline constexpr auto mesh = make_tag("mesh");
|
||||
inline constexpr auto integration = make_tag("integration");
|
||||
inline constexpr auto solver = make_tag("solver");
|
||||
|
||||
inline constexpr auto gravity = sub_tag(physics, "gravity");
|
||||
inline constexpr auto hydro = sub_tag(physics, "hydro");
|
||||
inline constexpr auto jacobian = sub_tag(integration & physics , "jacobian");
|
||||
inline constexpr auto residuals = sub_tag(integration & physics , "residuals");
|
||||
inline constexpr auto h_refinement = sub_tag(mesh & solver , "h_refinement");
|
||||
inline constexpr auto volume = sub_tag(mesh & geometry , "volume");
|
||||
inline constexpr auto quadrature = sub_tag(mesh & geometry & solver , "quadrature");
|
||||
|
||||
inline constexpr auto analytic_comparison = sub_tag(solver & physics & residuals , "analytic_comparison");
|
||||
inline constexpr auto self_consistency = sub_tag(solver & physics , "self_consistency");
|
||||
|
||||
}
|
||||
|
||||
223
tests/test_main.cpp
Normal file
223
tests/test_main.cpp
Normal file
@@ -0,0 +1,223 @@
|
||||
#include <catch2/catch_session.hpp>
|
||||
#include <catch2/reporters/catch_reporter_streaming_base.hpp>
|
||||
#include <catch2/reporters/catch_reporter_registrars.hpp>
|
||||
#include <catch2/catch_test_case_info.hpp>
|
||||
#include <iostream>
|
||||
#include <mfem.hpp>
|
||||
#include <string>
|
||||
#include <iomanip>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
|
||||
#include <fourdst/config/config.h>
|
||||
#include <CLI/CLI.hpp>
|
||||
|
||||
import mean_field;
|
||||
import test_helpers;
|
||||
|
||||
class CheckReporter : public Catch::StreamingReporterBase {
|
||||
// Accumulate failure messages for the current test case
|
||||
std::vector<std::string> m_currentFailures;
|
||||
|
||||
public:
|
||||
using StreamingReporterBase::StreamingReporterBase;
|
||||
|
||||
static std::string getDescription() {
|
||||
return "Fixed-width table reporter with detailed assertion failures.";
|
||||
}
|
||||
|
||||
void testRunStarting(Catch::TestRunInfo const& _testRunInfo) override {
|
||||
StreamingReporterBase::testRunStarting(_testRunInfo);
|
||||
|
||||
std::cout << '\n';
|
||||
std::cout << std::left << std::setw(55) << "Test Case Name"
|
||||
<< "Status "
|
||||
<< std::right << std::setw(8) << "Passed"
|
||||
<< std::setw(8) << "Failed" << '\n';
|
||||
std::cout << std::string(81, '-') << '\n';
|
||||
}
|
||||
|
||||
// 1. Hook into every assertion to catch failures
|
||||
void assertionEnded(Catch::AssertionStats const& assertionStats) override {
|
||||
StreamingReporterBase::assertionEnded(assertionStats);
|
||||
|
||||
// If the assertion failed, build a detailed message
|
||||
if (!assertionStats.assertionResult.isOk()) {
|
||||
auto const& result = assertionStats.assertionResult;
|
||||
std::ostringstream oss;
|
||||
|
||||
// Format: -> FAILED: [file:line]
|
||||
oss << " \033[31m-> FAILED:\033[0m "
|
||||
<< result.getSourceInfo().file << ":" << result.getSourceInfo().line << '\n';
|
||||
|
||||
// Print the macro used (e.g., REQUIRE, CHECK) and the expression
|
||||
oss << " " << result.getTestMacroName() << "( " << result.getExpression() << " )\n";
|
||||
|
||||
// Print what it actually evaluated to (e.g., 1 == 2)
|
||||
if (result.hasExpandedExpression()) {
|
||||
oss << " with expansion:\n"
|
||||
<< " " << result.getExpandedExpression() << '\n';
|
||||
}
|
||||
|
||||
// Capture any INFO() messages attached to this assertion
|
||||
for (auto const& msg : assertionStats.infoMessages) {
|
||||
oss << " info: " << msg.message << '\n';
|
||||
}
|
||||
|
||||
m_currentFailures.push_back(oss.str());
|
||||
}
|
||||
}
|
||||
|
||||
void testCaseEnded(Catch::TestCaseStats const& stats) override {
|
||||
StreamingReporterBase::testCaseEnded(stats);
|
||||
|
||||
bool passed = stats.totals.assertions.allPassed();
|
||||
std::string mark = passed ? "\033[32m✓\033[0m" : "\033[31m✗\033[0m";
|
||||
|
||||
std::string name = stats.testInfo->name;
|
||||
if (name.length() > 53) {
|
||||
name = name.substr(0, 50) + "...";
|
||||
}
|
||||
|
||||
// Print the table row
|
||||
std::cout << std::left << std::setw(55) << name
|
||||
<< mark << " "
|
||||
<< std::right << std::setw(8) << stats.totals.assertions.passed
|
||||
<< std::setw(8) << stats.totals.assertions.failed << '\n';
|
||||
|
||||
// 2. Print all accumulated failures under the row
|
||||
if (!m_currentFailures.empty()) {
|
||||
std::cout << '\n';
|
||||
for (auto const& failure : m_currentFailures) {
|
||||
std::cout << failure << '\n';
|
||||
}
|
||||
// Add a separator so multiple failing tests don't blur together
|
||||
std::cout << std::string(81, '-') << '\n';
|
||||
|
||||
// Clear the buffer for the next test case
|
||||
m_currentFailures.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void testRunEnded(Catch::TestRunStats const& _testRunStats) override {
|
||||
StreamingReporterBase::testRunEnded(_testRunStats);
|
||||
|
||||
std::cout << std::string(81, '=') << '\n';
|
||||
|
||||
auto const& tc = _testRunStats.totals.testCases;
|
||||
auto const& as = _testRunStats.totals.assertions;
|
||||
|
||||
std::string tc_passed_str = tc.passed > 0 ? "\033[32m" + std::to_string(tc.passed) + " passed\033[0m" : "0 passed";
|
||||
std::string tc_failed_str = tc.failed > 0 ? "\033[31m" + std::to_string(tc.failed) + " failed\033[0m" : "0 failed";
|
||||
|
||||
std::string as_passed_str = as.passed > 0 ? "\033[32m" + std::to_string(as.passed) + " passed\033[0m" : "0 passed";
|
||||
std::string as_failed_str = as.failed > 0 ? "\033[31m" + std::to_string(as.failed) + " failed\033[0m" : "0 failed";
|
||||
|
||||
std::cout << "Test Cases: " << tc_passed_str << ", " << tc_failed_str << ", " << tc.total() << " total\n";
|
||||
std::cout << "Assertions: " << as_passed_str << ", " << as_failed_str << ", " << as.total() << " total\n\n";
|
||||
}
|
||||
};
|
||||
CATCH_REGISTER_REPORTER("check", CheckReporter)
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
fourdst::config::Config<mean_field::utils::Args> cfg;
|
||||
CLI::App app{"Mean Field Tests"};
|
||||
|
||||
app.allow_extras();
|
||||
app.set_help_flag("--config-help", "Show mean-field configuration options");
|
||||
fourdst::config::register_as_cli(cfg, app);
|
||||
|
||||
std::vector<std::string> config_arguments;
|
||||
std::vector<std::string> forced_catch_arguments;
|
||||
config_arguments.emplace_back(argv[0]);
|
||||
|
||||
bool parsing_catch_arguments = false;
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
if (std::string_view(argv[i]) == "--catch2") {
|
||||
parsing_catch_arguments = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsing_catch_arguments) {
|
||||
forced_catch_arguments.emplace_back(argv[i]);
|
||||
} else {
|
||||
config_arguments.emplace_back(argv[i]);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<const char*> config_argv;
|
||||
config_argv.reserve(config_arguments.size());
|
||||
|
||||
for (const std::string& argument : config_arguments) {
|
||||
config_argv.push_back(argument.c_str());
|
||||
}
|
||||
|
||||
try {
|
||||
app.parse(static_cast<int>(config_argv.size()), config_argv.data());
|
||||
} catch (const CLI::ParseError& error) {
|
||||
return app.exit(error);
|
||||
}
|
||||
|
||||
std::vector<std::string> catch_arguments;
|
||||
catch_arguments.emplace_back(argv[0]);
|
||||
|
||||
for (const std::string& argument : app.remaining()) {
|
||||
catch_arguments.push_back(argument);
|
||||
}
|
||||
|
||||
for (const std::string& argument : forced_catch_arguments) {
|
||||
catch_arguments.push_back(argument);
|
||||
}
|
||||
|
||||
const auto is_reporter_option = [](const std::string& argument) {
|
||||
return argument == "-r" || argument == "--reporter" || argument.starts_with("-r=") || argument.starts_with("--reporter=");
|
||||
};
|
||||
|
||||
if (const bool has_reporter = std::ranges::any_of(catch_arguments, is_reporter_option); !has_reporter) {
|
||||
catch_arguments.emplace_back("--reporter");
|
||||
catch_arguments.emplace_back("check");
|
||||
}
|
||||
|
||||
std::vector<const char*> catch_argv;
|
||||
catch_argv.reserve(catch_arguments.size());
|
||||
|
||||
for (const std::string& argument : catch_arguments) {
|
||||
catch_argv.push_back(argument.c_str());
|
||||
}
|
||||
|
||||
Catch::Session session;
|
||||
|
||||
if (const int catch_parse_result = session.applyCommandLine(static_cast<int>(catch_argv.size()), catch_argv.data()); catch_parse_result != 0) {
|
||||
return catch_parse_result;
|
||||
}
|
||||
|
||||
mfem::Mpi::Init(argc, argv);
|
||||
|
||||
constexpr std::string device_config = "cpu";
|
||||
mfem::Device device(device_config);
|
||||
|
||||
const int hdiv_max_q1d = mfem::DeviceDofQuadLimits::Get().HDIV_MAX_Q1D;
|
||||
std::cout << "H(div) maximum Q1D = " << hdiv_max_q1d << '\n';
|
||||
std::cout << "Approximate maximum safe integration order = " << 2 * hdiv_max_q1d - 1 << '\n';
|
||||
|
||||
mean_field::utils::Args test_args = cfg.main();
|
||||
|
||||
if (app.count("--mesh_file") == 0) {
|
||||
test_args.mesh_file = "sandbox.smesh";
|
||||
}
|
||||
|
||||
if (app.count("--p.rtol") == 0) {
|
||||
test_args.p.rtol = 1.0e-12;
|
||||
}
|
||||
|
||||
if (app.count("--p.atol") == 0) {
|
||||
test_args.p.atol = 1.0e-12;
|
||||
}
|
||||
|
||||
test_utils::set_args(std::move(test_args));
|
||||
|
||||
return session.run();
|
||||
}
|
||||
Reference in New Issue
Block a user