Files
MeanField/tests/operators/gravity_field.cpp
Emily Boudreaux 0f3ca8050b feat(field-support): added field support system, mid migration
currently the barotope and the pressure force operator are migrated to the new support system
2026-08-23 10:13:53 -04:00

3560 lines
152 KiB
C++

#include "profile.h"
#include <array>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <cmath>
#include <mfem.hpp>
import mean_field;
import test_helpers;
using namespace mean_field;
using Catch::Matchers::WithinAbs;
namespace {
namespace blocks = utils::blocks;
using form = blocks::gravity_field_form;
constexpr auto density_block = blocks::get_value_block<form>(blocks::density_field.mass_term);
constexpr auto displacement_block = blocks::get_value_block<form>(blocks::displacement_field.geometry_term);
constexpr auto gravity_gradient_block = blocks::get_value_block<form>(blocks::gravity_field.gradient_term);
constexpr auto gravity_potential_block = blocks::get_value_block<form>(blocks::gravity_field.poisson_term);
constexpr auto gravity_gradient_residual_block =
blocks::get_residual_block<form>(blocks::gravity_field.gradient_term);
constexpr auto gravity_poisson_residual_block =
blocks::get_residual_block<form>(blocks::gravity_field.poisson_term);
blocks::form_layout<form> make_gravity_layout(const fem::FEM &f) {
const std::array<int, form::value_block_count> value_sizes{
f.densityFes->GetTrueVSize(), f.displacementFes->GetTrueVSize(), f.gravityFluxFes->GetTrueVSize(),
f.gravityPotentialFes->GetTrueVSize()
};
const std::array<int, form::residual_block_count> residual_sizes{
f.gravityFluxFes->GetTrueVSize(), f.gravityPotentialFes->GetTrueVSize()
};
return blocks::form_layout<form>(value_sizes, residual_sizes);
}
template <typename Block>
void set_block(
mfem::Vector &vector,
const mfem::Array<int> &offsets,
const Block block,
const mfem::Vector &values
) {
const int block_id = block;
const int begin = offsets[block_id];
const int size = offsets[block_id + 1] - begin;
REQUIRE(values.Size() == size);
for (int i = 0; i < size; ++i)
vector(begin + i) = values(i);
}
template <typename Block>
mfem::Vector get_block(
const mfem::Vector &vector,
const mfem::Array<int> &offsets,
const Block block
) {
const int block_id = block;
const int begin = offsets[block_id];
const int size = offsets[block_id + 1] - begin;
mfem::Vector result(size);
for (int i = 0; i < size; ++i)
result(i) = vector(begin + i);
return result;
}
mfem::Vector make_test_vector(
const int size,
const double phase
) {
mfem::Vector vector(size);
for (int i = 0; i < size; ++i)
vector(i) = 0.4 * std::sin(0.37 * static_cast<double>(i + 1) + phase) +
0.2 * std::cos(0.19 * static_cast<double>(i + 1) - phase);
return vector;
}
mfem::Vector make_displacement(const fem::FEM &f) {
auto displacement_function = [](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(3);
value(0) = 0.015 * position(0) + 0.004 * position(1);
value(1) = -0.003 * position(0) + 0.012 * position(1);
value(2) = -0.008 * position(2);
};
mfem::VectorFunctionCoefficient coefficient(3, displacement_function);
mfem::ParGridFunction displacement(f.displacementFes.get());
mfem::Vector displacement_true;
displacement.ProjectCoefficient(coefficient);
displacement.GetTrueDofs(displacement_true);
return displacement_true;
}
mfem::Vector make_constant_density(
const fem::FEM &f,
const double value
) {
mfem::ConstantCoefficient coefficient(value);
mfem::ParGridFunction density(f.densityFes.get());
mfem::Vector density_true;
density.ProjectCoefficient(coefficient);
density.GetTrueDofs(density_true);
return density_true;
}
mfem::Vector make_vacuum_density(
const fem::FEM &f,
const double value
) {
mfem::ParGridFunction density(f.densityFes.get());
density = 0.0;
mfem::Array<int> element_dofs;
mfem::Vector element_values;
for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) {
if (f.mesh->GetAttribute(element_id) != f.domainMapperStateless->GetVacuumElementAttribute())
continue;
f.densityFes->GetElementDofs(element_id, element_dofs);
element_values.SetSize(element_dofs.Size());
element_values = value;
density.SetSubVector(element_dofs, element_values);
}
mfem::Vector density_true;
density.GetTrueDofs(density_true);
return density_true;
}
double relative_difference(
const mfem::Vector &lhs,
const mfem::Vector &rhs
) {
REQUIRE(lhs.Size() == rhs.Size());
mfem::Vector difference(lhs);
difference -= rhs;
return difference.Norml2() / std::max({lhs.Norml2(), rhs.Norml2(), 1.0e-14});
}
mfem::Vector make_core_supported_gravity_gradient(const fem::FEM &f) {
constexpr double support_radius = 0.15 * utils::RADIUS;
constexpr double support_radius_squared = support_radius * support_radius;
auto field_function = [](const mfem::Vector &position, mfem::Vector &value) {
const double radius_squared = position * position;
value.SetSize(3);
value = 0.0;
if (radius_squared >= support_radius_squared)
return;
const double normalized_radius_squared = radius_squared / support_radius_squared;
const double envelope = std::pow(1.0 - normalized_radius_squared, 3.0);
value(0) = envelope;
value(1) = -0.4 * envelope;
value(2) = 0.7 * envelope;
};
mfem::VectorFunctionCoefficient coefficient(3, field_function);
mfem::ParGridFunction gravity_gradient(f.gravityFluxFes.get());
mfem::Vector gravity_gradient_true;
gravity_gradient.ProjectCoefficient(coefficient);
gravity_gradient.GetTrueDofs(gravity_gradient_true);
return gravity_gradient_true;
}
double global_norm(
const mfem::Vector &vector,
MPI_Comm communicator
) {
REQUIRE(vector.Size() > 0);
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_relative_difference(
const mfem::Vector &lhs,
const mfem::Vector &rhs,
MPI_Comm communicator
) {
REQUIRE(lhs.Size() == rhs.Size());
mfem::Vector difference(lhs);
difference -= rhs;
const double difference_norm = global_norm(difference, communicator);
const double lhs_norm = global_norm(lhs, communicator);
const double rhs_norm = global_norm(rhs, communicator);
return difference_norm / std::max({lhs_norm, rhs_norm, 1.0e-14});
}
struct StatelessHDivMassReference {
mfem::Vector total_action;
mfem::Vector stellar_action;
mfem::Vector vacuum_action;
long long stellar_elements{0};
long long vacuum_elements{0};
long long stellar_quadrature_points{0};
long long vacuum_quadrature_points{0};
double minimum_stellar_determinant{std::numeric_limits<double>::infinity()};
double maximum_stellar_determinant{0.0};
double minimum_vacuum_determinant{std::numeric_limits<double>::infinity()};
double maximum_vacuum_determinant{0.0};
};
void reference_true_to_local(
const mfem::ParFiniteElementSpace &fes,
const mfem::Vector &true_vector,
mfem::Vector &local_vector
) {
local_vector.SetSize(fes.GetVSize());
const mfem::Operator *prolongation = fes.GetProlongationMatrix();
if (prolongation != nullptr) {
prolongation->Mult(true_vector, local_vector);
} else {
local_vector = true_vector;
}
}
void reference_local_to_true(
const mfem::ParFiniteElementSpace &fes,
const mfem::Vector &local_vector,
mfem::Vector &true_vector
) {
true_vector.SetSize(fes.GetTrueVSize());
true_vector = 0.0;
const mfem::Operator *prolongation = fes.GetProlongationMatrix();
if (prolongation != nullptr) {
prolongation->MultTranspose(local_vector, true_vector);
} else {
true_vector = local_vector;
}
}
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());
}
const mfem::IntegrationRule &get_stateless_hdiv_reference_rule(
const fem::FEM &f,
const mfem::FiniteElement &element,
const mfem::ElementTransformation &transformation
) {
const bool is_vacuum = transformation.Attribute == f.domainMapperStateless->GetVacuumElementAttribute();
const quadrature::Query query{
.term = quadrature::Term::gravity_hdiv_mass,
.role = quadrature::QuadratureRole::discretization,
.domain = utils::DOMAINS::ALL,
.mapping = is_vacuum ? quadrature::MappingKind::kelvin : quadrature::MappingKind::general,
.trial_order = element.GetOrder(),
.test_order = element.GetOrder(),
.coefficient_order = 0,
.geometry_weight_order = transformation.OrderW()
};
return *f.quadratureFactory->get(query, transformation.GetGeometryType()).integration_rule;
}
mfem::Vector make_full_support_gravity_gradient(const fem::FEM &f) {
mfem::Vector gravity_gradient(f.gravityFluxFes->GetTrueVSize());
for (int i = 0; i < gravity_gradient.Size(); ++i) {
const double index = static_cast<double>(i + 1);
gravity_gradient(i) = std::sin(0.37 * index) + 0.31 * std::cos(0.19 * index);
}
return gravity_gradient;
}
mfem::Vector make_stateless_reference_displacement(
const fem::FEM &f,
const bool deformed
) {
mfem::ParGridFunction displacement(f.displacementFes.get());
displacement = 0.0;
if (deformed) {
auto displacement_function = [](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(3);
value(0) = 0.04 * position(0) + 0.01 * position(1) * position(2);
value(1) = -0.03 * position(1) + 0.008 * position(0) * position(2);
value(2) = 0.02 * position(2) - 0.006 * position(0) * position(1);
};
mfem::VectorFunctionCoefficient displacement_coefficient(3, displacement_function);
displacement.ProjectCoefficient(displacement_coefficient);
}
mfem::Vector displacement_true;
displacement.GetTrueDofs(displacement_true);
return displacement_true;
}
std::string_view mapping_status_name(const mapping::MappingStatus status) {
switch (status) {
case mapping::MappingStatus::valid:
return "valid";
case mapping::MappingStatus::invalid_dimension:
return "invalid_dimension";
case mapping::MappingStatus::non_finite_input:
return "non_finite_input";
case mapping::MappingStatus::invalid_reference_radius:
return "invalid_reference_radius";
case mapping::MappingStatus::at_compactified_infinity:
return "at_compactified_infinity";
case mapping::MappingStatus::outside_reference_domain:
return "outside_reference_domain";
case mapping::MappingStatus::non_finite_result:
return "non_finite_result";
case mapping::MappingStatus::non_positive_determinant:
return "non_positive_determinant";
}
return "unknown";
}
StatelessHDivMassReference evaluate_stateless_hdiv_mass_quadrature_reference(
const fem::FEM &fem,
const mfem::Vector &gravity_gradient_true,
const mfem::Vector &displacement_true
) {
MFEM_VERIFY(fem.domainMapperStateless != nullptr, "The stateless domain mapper is unavailable.");
MFEM_VERIFY(fem.compactificationFes != nullptr, "The compactification finite-element space is unavailable.");
MFEM_VERIFY(fem.compactificationCoordinate != nullptr, "The compactification coordinate is unavailable.");
MFEM_VERIFY(
gravity_gradient_true.Size() == fem.gravityFluxFes->GetTrueVSize(),
"The gravity-gradient vector has the wrong size."
);
MFEM_VERIFY(
displacement_true.Size() == fem.displacementFes->GetTrueVSize(),
"The displacement vector has the wrong size."
);
mfem::Vector gravity_gradient_local;
mfem::Vector displacement_local;
reference_true_to_local(*fem.gravityFluxFes, gravity_gradient_true, gravity_gradient_local);
reference_true_to_local(*fem.displacementFes, displacement_true, displacement_local);
mfem::Vector total_local(fem.gravityFluxFes->GetVSize());
mfem::Vector stellar_local(fem.gravityFluxFes->GetVSize());
mfem::Vector vacuum_local(fem.gravityFluxFes->GetVSize());
total_local = 0.0;
stellar_local = 0.0;
vacuum_local = 0.0;
StatelessHDivMassReference reference;
mapping::DomainMapperStateless::Workspace workspace(fem.mesh->Dimension());
mapping::VolumeMappingContext mapping_context;
mfem::Array<int> gravity_dofs;
mfem::Array<int> displacement_dofs;
mfem::Array<int> compactification_dofs;
mfem::Vector element_gravity_gradient;
mfem::Vector element_displacement;
mfem::Vector element_compactification;
mfem::Vector element_action;
mfem::Vector quadrature_flux;
mfem::Vector mapped_quadrature_flux;
mfem::DenseMatrix vector_shape;
mfem::DenseMatrix mapped_mass_tensor;
const int vacuum_attribute = fem.domainMapperStateless->GetVacuumElementAttribute();
for (int element_id = 0; element_id < fem.mesh->GetNE(); ++element_id) {
const mfem::FiniteElement &gravity_element = *fem.gravityFluxFes->GetFE(element_id);
const mfem::FiniteElement &displacement_element = *fem.displacementFes->GetFE(element_id);
const mfem::FiniteElement &compactification_element = *fem.compactificationFes->GetFE(element_id);
mfem::ElementTransformation &transformation = *fem.mesh->GetElementTransformation(element_id);
const bool is_vacuum = transformation.Attribute == vacuum_attribute;
mfem::DofTransformation *gravity_dof_transformation =
fem.gravityFluxFes->GetElementVDofs(element_id, gravity_dofs);
mfem::DofTransformation *displacement_dof_transformation =
fem.displacementFes->GetElementVDofs(element_id, displacement_dofs);
mfem::DofTransformation *compactification_dof_transformation =
fem.compactificationFes->GetElementDofs(element_id, compactification_dofs);
gravity_gradient_local.GetSubVector(gravity_dofs, element_gravity_gradient);
displacement_local.GetSubVector(displacement_dofs, element_displacement);
fem.compactificationCoordinate->GetSubVector(compactification_dofs, element_compactification);
if (gravity_dof_transformation != nullptr) {
gravity_dof_transformation->InvTransformPrimal(element_gravity_gradient);
}
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 =
mapping::ElementDisplacementDataFromElementVDofs(displacement_element, element_displacement);
const mapping::ElementCompactificationData compactification_data(
compactification_element, element_compactification
);
const mapping::ElementMappingData mapping_data{
.displacement = displacement_data, .compactification = compactification_data
};
const int gravity_dof_count = gravity_element.GetDof();
const int dimension = transformation.GetSpaceDim();
element_action.SetSize(gravity_dof_count);
element_action = 0.0;
quadrature_flux.SetSize(dimension);
mapped_quadrature_flux.SetSize(dimension);
vector_shape.SetSize(gravity_dof_count, dimension);
mapped_mass_tensor.SetSize(dimension, dimension);
const mfem::IntegrationRule &integration_rule =
get_stateless_hdiv_reference_rule(fem, gravity_element, transformation);
for (int quadrature_point = 0; quadrature_point < integration_rule.GetNPoints(); ++quadrature_point) {
const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(quadrature_point);
transformation.SetIntPoint(&integration_point);
const mapping::MappingStatus status = fem.domainMapperStateless->EvaluateVolume(
mapping_data, transformation, integration_point, workspace, mapping_context
);
MFEM_VERIFY(
status == mean_field::mapping::MappingStatus::valid,
"Stateless mapping failed while evaluating the H(div) "
"quadrature reference."
<< "\nElement ID = " << element_id << "\nElement attribute = " << transformation.Attribute
<< "\nQuadrature point = " << quadrature_point
<< "\nMapping status = " << mapping_status_name(status)
);
gravity_element.CalcVShape(transformation, vector_shape);
mean_field::mapping::ComputeHDivMassTensor(mapping_context.mapping, mapped_mass_tensor);
// Evaluate B*x at this quadrature point.
vector_shape.MultTranspose(element_gravity_gradient, quadrature_flux);
// Apply the mapped H(div) mass tensor.
mapped_mass_tensor.Mult(quadrature_flux, mapped_quadrature_flux);
const double weight = integration_point.weight * transformation.Weight();
// Accumulate B^T*D*B*x directly.
for (int dof = 0; dof < gravity_dof_count; ++dof) {
double contribution = 0.0;
for (int component = 0; component < dimension; ++component) {
contribution += vector_shape(dof, component) * mapped_quadrature_flux(component);
}
element_action(dof) += weight * contribution;
}
const double mapping_determinant = mapping_context.mapping.mapping_determinant;
MFEM_VERIFY(
std::isfinite(mapping_determinant) && mapping_determinant > 0.0,
"The quadrature reference encountered an invalid mapping "
"determinant."
);
if (is_vacuum) {
reference.minimum_vacuum_determinant =
std::min(reference.minimum_vacuum_determinant, mapping_determinant);
reference.maximum_vacuum_determinant =
std::max(reference.maximum_vacuum_determinant, mapping_determinant);
++reference.vacuum_quadrature_points;
} else {
reference.minimum_stellar_determinant =
std::min(reference.minimum_stellar_determinant, mapping_determinant);
reference.maximum_stellar_determinant =
std::max(reference.maximum_stellar_determinant, mapping_determinant);
++reference.stellar_quadrature_points;
}
}
if (gravity_dof_transformation != nullptr) {
gravity_dof_transformation->TransformDual(element_action);
}
total_local.AddElementVector(gravity_dofs, element_action);
if (is_vacuum) {
vacuum_local.AddElementVector(gravity_dofs, element_action);
++reference.vacuum_elements;
} else {
stellar_local.AddElementVector(gravity_dofs, element_action);
++reference.stellar_elements;
}
}
reference_local_to_true(*fem.gravityFluxFes, total_local, reference.total_action);
reference_local_to_true(*fem.gravityFluxFes, stellar_local, reference.stellar_action);
reference_local_to_true(*fem.gravityFluxFes, vacuum_local, reference.vacuum_action);
const MPI_Comm communicator = fem.gravityFluxFes->GetComm();
const long long local_counts[4]{
reference.stellar_elements, reference.vacuum_elements, reference.stellar_quadrature_points,
reference.vacuum_quadrature_points
};
long long global_counts[4]{};
MPI_Allreduce(local_counts, global_counts, 4, MPI_LONG_LONG, MPI_SUM, communicator);
reference.stellar_elements = global_counts[0];
reference.vacuum_elements = global_counts[1];
reference.stellar_quadrature_points = global_counts[2];
reference.vacuum_quadrature_points = global_counts[3];
const double local_minimums[2]{reference.minimum_stellar_determinant, reference.minimum_vacuum_determinant};
const double local_maximums[2]{reference.maximum_stellar_determinant, reference.maximum_vacuum_determinant};
double global_minimums[2]{};
double global_maximums[2]{};
MPI_Allreduce(local_minimums, global_minimums, 2, MPI_DOUBLE, MPI_MIN, communicator);
MPI_Allreduce(local_maximums, global_maximums, 2, MPI_DOUBLE, MPI_MAX, communicator);
reference.minimum_stellar_determinant = global_minimums[0];
reference.minimum_vacuum_determinant = global_minimums[1];
reference.maximum_stellar_determinant = global_maximums[0];
reference.maximum_vacuum_determinant = global_maximums[1];
return reference;
}
using gravity_form = blocks::gravity_field_form;
using gravity_layout = blocks::form_layout<gravity_form>;
gravity_layout make_gravity_jacobian_layout(const fem::FEM &f) {
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()
};
return gravity_layout(value_sizes, residual_sizes);
}
template <int index>
void set_value_block(
mfem::Vector &vector,
const gravity_layout &layout,
const blocks::value_block<index> block,
const mfem::Vector &values
) {
REQUIRE(values.Size() == layout.size(block));
for (int i = 0; i < values.Size(); ++i)
vector(layout.offset(block) + i) = values(i);
}
template <int index>
mfem::Vector get_value_block(
const mfem::Vector &vector,
const gravity_layout &layout,
const blocks::value_block<index> block
) {
mfem::Vector values(layout.size(block));
for (int i = 0; i < values.Size(); ++i)
values(i) = vector(layout.offset(block) + i);
return values;
}
template <int index>
mfem::Vector get_residual_block(
const mfem::Vector &vector,
const gravity_layout &layout,
const blocks::residual_block<index> block
) {
mfem::Vector values(layout.size(block));
for (int i = 0; i < values.Size(); ++i)
values(i) = vector(layout.offset(block) + i);
return values;
}
template <int index>
void fill_value_block(
mfem::Vector &vector,
const gravity_layout &layout,
const blocks::value_block<index> block,
const double scale,
const double phase
) {
for (int i = 0; i < layout.size(block); ++i) {
const double index_value = static_cast<double>(i + 1);
vector(layout.offset(block) + i) =
scale * (std::sin(0.31 * index_value + phase) + 0.37 * std::cos(0.17 * index_value - phase));
}
}
mfem::Vector make_gravity_jacobian_state(
const fem::FEM &f,
const gravity_layout &layout,
const bool deformed
) {
mfem::Vector state(layout.value_offsets().Last());
state = 0.0;
fill_value_block(state, layout, density_block, 0.7, 0.11);
fill_value_block(state, layout, gravity_gradient_block, 0.4, 0.23);
fill_value_block(state, layout, gravity_potential_block, 0.3, 0.37);
const mfem::Vector displacement = make_stateless_reference_displacement(f, deformed);
set_value_block(state, layout, displacement_block, displacement);
return state;
}
mfem::Vector make_density_direction(const gravity_layout &layout) {
mfem::Vector direction(layout.value_offsets().Last());
direction = 0.0;
fill_value_block(direction, layout, density_block, 0.13, 0.19);
return direction;
}
mfem::Vector make_gravity_gradient_direction(const gravity_layout &layout) {
mfem::Vector direction(layout.value_offsets().Last());
direction = 0.0;
fill_value_block(direction, layout, gravity_gradient_block, 0.11, 0.29);
return direction;
}
mfem::Vector make_gravity_potential_direction(const gravity_layout &layout) {
mfem::Vector direction(layout.value_offsets().Last());
direction = 0.0;
fill_value_block(direction, layout, gravity_potential_block, 0.09, 0.41);
return direction;
}
mfem::Vector make_combined_fixed_geometry_direction(const gravity_layout &layout) {
mfem::Vector direction = make_density_direction(layout);
const mfem::Vector gravity_gradient_direction = make_gravity_gradient_direction(layout);
const mfem::Vector gravity_potential_direction = make_gravity_potential_direction(layout);
direction += gravity_gradient_direction;
direction += gravity_potential_direction;
return direction;
}
mfem::Vector make_displacement_direction(
const gravity_layout &layout,
const mfem::Vector &displacement_direction
) {
mfem::Vector direction(layout.value_offsets().Last());
direction = 0.0;
set_value_block(direction, layout, displacement_block, displacement_direction);
return direction;
}
mfem::Vector evaluate_centered_difference(
operators::GravityFieldOperator &gravity_operator,
const mfem::Vector &state,
const mfem::Vector &direction,
const double step
) {
mfem::Vector plus_state(state);
mfem::Vector minus_state(state);
plus_state.Add(step, direction);
minus_state.Add(-step, direction);
mfem::Vector plus_residual;
mfem::Vector minus_residual;
gravity_operator.Mult(plus_state, plus_residual);
gravity_operator.Mult(minus_state, minus_residual);
mfem::Vector difference(plus_residual);
difference -= minus_residual;
difference /= 2.0 * step;
return difference;
}
struct HdivMassVariationTestFields {
mfem::Vector gravity_gradient;
mfem::Vector displacement;
mfem::Vector displacement_direction_1;
mfem::Vector displacement_direction_2;
};
double global_relative_error(
const mfem::Vector &computed,
const mfem::Vector &reference,
MPI_Comm communicator
) {
REQUIRE(computed.Size() == reference.Size());
mfem::Vector difference(computed);
difference -= reference;
return global_norm(difference, communicator) / std::max(global_norm(reference, communicator), 1.0e-14);
}
HdivMassVariationTestFields make_hdiv_mass_variation_test_fields(const fem::FEM &f) {
const int dimension = f.mesh->Dimension();
auto gravity_gradient_function = [](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(3);
value(0) = 0.4 + 0.18 * position(0) - 0.07 * position(1) * position(2);
value(1) = -0.3 + 0.11 * position(1) + 0.05 * position(0) * position(2);
value(2) = 0.2 - 0.09 * position(2) + 0.04 * position(0) * position(1);
};
auto displacement_function = [](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(3);
value(0) = 0.025 * position(0) + 0.006 * position(1) * position(2);
value(1) = -0.018 * position(1) + 0.005 * position(0) * position(2);
value(2) = 0.014 * position(2) + 0.004 * position(0) * position(1);
};
auto displacement_direction_1_function = [](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(3);
value(0) = 0.16 * position(0) + 0.03 * position(1);
value(1) = -0.11 * position(1) + 0.02 * position(2);
value(2) = 0.13 * position(2) - 0.025 * position(0);
};
auto displacement_direction_2_function = [](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(3);
value(0) = -0.07 * position(1) + 0.025 * position(2);
value(1) = 0.09 * position(0) + 0.04 * position(2);
value(2) = -0.08 * position(2) + 0.03 * position(0) * position(1);
};
mfem::VectorFunctionCoefficient gravity_gradient_coefficient(dimension, gravity_gradient_function);
mfem::VectorFunctionCoefficient displacement_coefficient(dimension, displacement_function);
mfem::VectorFunctionCoefficient displacement_direction_1_coefficient(
dimension, displacement_direction_1_function
);
mfem::VectorFunctionCoefficient displacement_direction_2_coefficient(
dimension, displacement_direction_2_function
);
mfem::ParGridFunction gravity_gradient_grid(f.gravityFluxFes.get());
mfem::ParGridFunction displacement_grid(f.displacementFes.get());
mfem::ParGridFunction displacement_direction_1_grid(f.displacementFes.get());
mfem::ParGridFunction displacement_direction_2_grid(f.displacementFes.get());
gravity_gradient_grid.ProjectCoefficient(gravity_gradient_coefficient);
displacement_grid.ProjectCoefficient(displacement_coefficient);
displacement_direction_1_grid.ProjectCoefficient(displacement_direction_1_coefficient);
displacement_direction_2_grid.ProjectCoefficient(displacement_direction_2_coefficient);
HdivMassVariationTestFields fields;
gravity_gradient_grid.GetTrueDofs(fields.gravity_gradient);
displacement_grid.GetTrueDofs(fields.displacement);
displacement_direction_1_grid.GetTrueDofs(fields.displacement_direction_1);
displacement_direction_2_grid.GetTrueDofs(fields.displacement_direction_2);
return fields;
}
mfem::Vector centered_hdiv_mass_geometry_difference(
const fem::FEM &f,
const mapping::DomainMapperStateless &domain_mapper,
const mfem::Vector &gravity_gradient,
const mfem::Vector &displacement,
const mfem::Vector &displacement_direction,
const double difference_step
) {
mfem::Vector plus_displacement(displacement);
mfem::Vector minus_displacement(displacement);
plus_displacement.Add(difference_step, displacement_direction);
minus_displacement.Add(-difference_step, displacement_direction);
mfem::Vector plus_action;
mfem::Vector minus_action;
operators::kernels::apply_mapped_hdiv_mass(f, domain_mapper, gravity_gradient, plus_displacement, plus_action);
operators::kernels::apply_mapped_hdiv_mass(
f, domain_mapper, gravity_gradient, minus_displacement, minus_action
);
plus_action -= minus_action;
plus_action /= 2.0 * difference_step;
return plus_action;
}
mfem::Vector make_source_variation_density(const fem::FEM &f) {
auto density_function = [](const mfem::Vector &position) {
return 1.2 + 0.16 * position(0) - 0.09 * position(1) + 0.07 * position(2) +
0.04 * position(0) * position(1);
};
mfem::FunctionCoefficient density_coefficient(density_function);
mfem::ParGridFunction density_grid(f.densityFes.get());
mfem::Vector density_true;
density_grid.ProjectCoefficient(density_coefficient);
density_grid.GetTrueDofs(density_true);
return density_true;
}
mfem::Vector centered_source_geometry_difference(
const fem::FEM &f,
const mapping::DomainMapperStateless &domain_mapper,
const mfem::Vector &density,
const mfem::Vector &displacement,
const mfem::Vector &displacement_direction,
const double difference_step
) {
mfem::Vector plus_displacement(displacement);
mfem::Vector minus_displacement(displacement);
plus_displacement.Add(difference_step, displacement_direction);
minus_displacement.Add(-difference_step, displacement_direction);
mfem::Vector plus_action;
mfem::Vector minus_action;
operators::kernels::apply_mapped_source(f, domain_mapper, density, plus_displacement, plus_action);
operators::kernels::apply_mapped_source(f, domain_mapper, density, minus_displacement, minus_action);
plus_action -= minus_action;
plus_action /= 2.0 * difference_step;
return plus_action;
}
double global_dot(
const mfem::Vector &left,
const mfem::Vector &right,
MPI_Comm communicator
) {
REQUIRE(left.Size() > 0);
REQUIRE(left.Size() == right.Size());
const double local_dot = left * right;
double global_dot_value = 0.0;
MPI_Allreduce(&local_dot, &global_dot_value, 1, MPI_DOUBLE, MPI_SUM, communicator);
return global_dot_value;
}
mfem::Vector make_secondary_gravity_gradient(const fem::FEM &f) {
auto gravity_gradient_function = [](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(3);
value(0) = -0.17 + 0.09 * position(1) + 0.03 * position(0) * position(2);
value(1) = 0.31 - 0.14 * position(0) + 0.05 * position(1) * position(2);
value(2) = -0.22 + 0.12 * position(2) - 0.04 * position(0) * position(1);
};
mfem::VectorFunctionCoefficient coefficient(f.mesh->Dimension(), gravity_gradient_function);
mfem::ParGridFunction grid_function(f.gravityFluxFes.get());
mfem::Vector true_dofs;
grid_function.ProjectCoefficient(coefficient);
grid_function.GetTrueDofs(true_dofs);
return true_dofs;
}
mfem::Vector make_secondary_source_density(const fem::FEM &f) {
auto density_function = [](const mfem::Vector &position) {
return 0.8 - 0.11 * position(0) + 0.13 * position(1) - 0.06 * position(2) +
0.03 * position(1) * position(2);
};
mfem::FunctionCoefficient coefficient(density_function);
mfem::ParGridFunction grid_function(f.densityFes.get());
mfem::Vector true_dofs;
grid_function.ProjectCoefficient(coefficient);
grid_function.GetTrueDofs(true_dofs);
return true_dofs;
}
mfem::Vector make_vacuum_only_density(
const fem::FEM &f,
const int vacuum_attribute
) {
mfem::ParGridFunction grid_function(f.densityFes.get());
grid_function = 0.0;
mfem::Array<int> element_dofs;
mfem::Vector element_values;
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 &element = *f.densityFes->GetFE(element_id);
mfem::DofTransformation *dof_transformation = f.densityFes->GetElementDofs(element_id, element_dofs);
element_values.SetSize(element.GetDof());
for (int i = 0; i < element_values.Size(); ++i)
element_values(i) = 0.9 + 0.01 * static_cast<double>(i);
if (dof_transformation != nullptr)
dof_transformation->TransformPrimal(element_values);
grid_function.SetSubVector(element_dofs, element_values);
}
mfem::Vector true_dofs;
grid_function.GetTrueDofs(true_dofs);
return true_dofs;
}
template <int index>
mfem::Vector make_read_only_value_view(
const mfem::Vector &vector,
const mfem::Array<int> &offsets,
const blocks::value_block<index>
) {
const int offset = offsets[index];
const int size = offsets[index + 1] - offset;
return mfem::Vector(const_cast<mfem::real_t *>(vector.GetData()) + offset, size);
}
void check_linearization_context_matches_state(
const operators::context::gravity_field::GravityFieldLinearizationContext &context,
const mfem::Vector &state,
const mfem::Array<int> &state_offsets,
MPI_Comm communicator
) {
using form = blocks::gravity_field_form;
constexpr auto density_block = utils::blocks::get_value_block<form>(blocks::density_field.mass_term);
constexpr auto displacement_block =
utils::blocks::get_value_block<form>(blocks::displacement_field.geometry_term);
constexpr auto gravity_gradient_block =
utils::blocks::get_value_block<form>(blocks::gravity_field.gradient_term);
const mfem::Vector density = make_read_only_value_view(state, state_offsets, density_block);
const mfem::Vector displacement = make_read_only_value_view(state, state_offsets, displacement_block);
const mfem::Vector gravity_gradient = make_read_only_value_view(state, state_offsets, gravity_gradient_block);
CHECK_THAT(
global_relative_vector_error(context.GetDensity(), density, communicator),
Catch::Matchers::WithinAbs(0.0, 0.0)
);
CHECK_THAT(
global_relative_vector_error(context.GetGeometryContext().GetDisplacement(), displacement, communicator),
Catch::Matchers::WithinAbs(0.0, 0.0)
);
CHECK_THAT(
global_relative_vector_error(context.GetGravityGradient(), gravity_gradient, communicator),
Catch::Matchers::WithinAbs(0.0, 0.0)
);
}
StatelessHDivMassReference assemble_legacy_hdiv_mass_reference(
const fem::FEM &f,
const mfem::Vector &gravity_gradient_true
) {
MFEM_VERIFY(f.mesh != nullptr, "The legacy H(div) reference requires a mesh.");
MFEM_VERIFY(f.gravityFluxFes != nullptr, "The legacy H(div) reference requires the RT finite-element space.");
MFEM_VERIFY(f.mapping != nullptr, "The legacy H(div) reference requires the legacy domain mapper.");
MFEM_VERIFY(f.domainMapperStateless != nullptr, "The vacuum attribute is unavailable.");
mfem::Vector gravity_gradient_local;
reference_true_to_local(*f.gravityFluxFes, gravity_gradient_true, gravity_gradient_local);
mfem::Vector total_local(f.gravityFluxFes->GetVSize());
mfem::Vector stellar_local(f.gravityFluxFes->GetVSize());
mfem::Vector vacuum_local(f.gravityFluxFes->GetVSize());
total_local = 0.0;
stellar_local = 0.0;
vacuum_local = 0.0;
StatelessHDivMassReference reference;
const int vacuum_attribute = f.domainMapperStateless->GetVacuumElementAttribute();
for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) {
const mfem::FiniteElement &gravity_element = *f.gravityFluxFes->GetFE(element_id);
mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(element_id);
MFEM_VERIFY(
transformation != nullptr, "The legacy H(div) reference received a null element "
"transformation."
);
const bool is_vacuum = transformation->Attribute == vacuum_attribute;
mfem::Array<int> gravity_dofs;
f.gravityFluxFes->GetElementVDofs(element_id, gravity_dofs);
mfem::Vector element_gravity_gradient;
gravity_gradient_local.GetSubVector(gravity_dofs, element_gravity_gradient);
const int gravity_dof_count = gravity_element.GetDof();
const int dimension = transformation->GetSpaceDim();
mfem::DenseMatrix element_matrix(gravity_dof_count);
mfem::DenseMatrix vector_shape(gravity_dof_count, dimension);
mfem::DenseMatrix mapping_jacobian(dimension);
mfem::DenseMatrix mapped_mass_tensor(dimension);
element_matrix = 0.0;
const mfem::IntegrationRule &integration_rule =
get_stateless_hdiv_reference_rule(f, gravity_element, *transformation);
for (int q = 0; q < integration_rule.GetNPoints(); ++q) {
const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q);
transformation->SetIntPoint(&integration_point);
/*
* This reproduces the legacy MappedHDivMassCoefficient.
*
* In particular, it does not evaluate or otherwise use
* f.compactificationCoordinate.
*/
f.mapping->ComputeJacobian(*transformation, mapping_jacobian);
const double mapping_determinant = mapping_jacobian.Det();
CAPTURE(element_id, q, transformation->Attribute);
REQUIRE(std::isfinite(mapping_determinant));
REQUIRE(mapping_determinant > 0.0);
mfem::MultAtB(mapping_jacobian, mapping_jacobian, mapped_mass_tensor);
mapped_mass_tensor *= 1.0 / std::abs(mapping_determinant);
gravity_element.CalcVShape(*transformation, vector_shape);
const double weight = integration_point.weight * transformation->Weight();
for (int i = 0; i < gravity_dof_count; ++i) {
for (int j = 0; j < gravity_dof_count; ++j) {
double entry = 0.0;
for (int row = 0; row < dimension; ++row) {
for (int column = 0; column < dimension; ++column) {
entry +=
vector_shape(i, row) * mapped_mass_tensor(row, column) * vector_shape(j, column);
}
}
element_matrix(i, j) += weight * entry;
}
}
if (is_vacuum) {
++reference.vacuum_quadrature_points;
} else {
++reference.stellar_quadrature_points;
}
}
mfem::Vector element_action(gravity_dof_count);
element_matrix.Mult(element_gravity_gradient, element_action);
total_local.AddElementVector(gravity_dofs, element_action);
if (is_vacuum) {
vacuum_local.AddElementVector(gravity_dofs, element_action);
++reference.vacuum_elements;
} else {
stellar_local.AddElementVector(gravity_dofs, element_action);
++reference.stellar_elements;
}
}
reference_local_to_true(*f.gravityFluxFes, total_local, reference.total_action);
reference_local_to_true(*f.gravityFluxFes, stellar_local, reference.stellar_action);
reference_local_to_true(*f.gravityFluxFes, vacuum_local, reference.vacuum_action);
MPI_Comm communicator = f.gravityFluxFes->GetComm();
const long long local_counts[4]{
reference.stellar_elements, reference.vacuum_elements, reference.stellar_quadrature_points,
reference.vacuum_quadrature_points
};
long long global_counts[4]{};
MPI_Allreduce(local_counts, global_counts, 4, MPI_LONG_LONG, MPI_SUM, communicator);
reference.stellar_elements = global_counts[0];
reference.vacuum_elements = global_counts[1];
reference.stellar_quadrature_points = global_counts[2];
reference.vacuum_quadrature_points = global_counts[3];
return reference;
}
} // namespace
TEST_CASE(
"Gravity Field Operator Mult Preserves Zero State And Static Blocks",
tags::unit &tags::solver &tags::gravity &tags::mfem_operators
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
physics::update_stiffness_matrix(f);
REQUIRE(f.domainMapperStateless != nullptr);
const blocks::form_layout<form> layout = make_gravity_layout(f);
operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(
f, *f.domainMapperStateless
);
operators::GravityFieldJacobianOperator gravity_jacobian(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()
);
operators::GravityFieldOperator gravity_operator(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian
);
mfem::Vector state(layout.value_offsets().Last());
mfem::Vector residual;
state = 0.0;
constexpr operators::context::gravity_field::GravityFieldRevisions revisions{
.discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1}
};
gravity_operator.Prepare(state, revisions);
gravity_operator.Mult(state, residual);
REQUIRE(residual.Size() == layout.residual_offsets().Last());
CHECK_THAT(residual.Norml2(), WithinAbs(0.0, 1.0e-14));
const mfem::Vector gravity_potential = make_test_vector(layout.size(gravity_potential_block), 0.31);
set_block(state, layout.value_offsets(), gravity_potential_block, gravity_potential);
gravity_operator.Mult(state, residual);
const mfem::Vector gradient_residual =
get_block(residual, layout.residual_offsets(), gravity_gradient_residual_block);
const mfem::Vector poisson_residual =
get_block(residual, layout.residual_offsets(), gravity_poisson_residual_block);
mfem::Vector expected_gradient_residual(layout.size(gravity_gradient_residual_block));
f.gravityContext.BT->Mult(gravity_potential, expected_gradient_residual);
CHECK_THAT(relative_difference(gradient_residual, expected_gradient_residual), WithinAbs(0.0, 1.0e-13));
CHECK_THAT(poisson_residual.Norml2(), WithinAbs(0.0, 1.0e-14));
state = 0.0;
const mfem::Vector gravity_gradient = make_test_vector(layout.size(gravity_gradient_block), 0.73);
set_block(state, layout.value_offsets(), gravity_gradient_block, gravity_gradient);
gravity_operator.Mult(state, residual);
const mfem::Vector gradient_only_residual =
get_block(residual, layout.residual_offsets(), gravity_gradient_residual_block);
const mfem::Vector poisson_only_residual =
get_block(residual, layout.residual_offsets(), gravity_poisson_residual_block);
mfem::Vector expected_poisson_residual(layout.size(gravity_poisson_residual_block));
f.gravityContext.b_form->Mult(gravity_gradient, expected_poisson_residual);
CHECK(gradient_only_residual.Norml2() > 0.0);
CHECK_THAT(relative_difference(poisson_only_residual, expected_poisson_residual), WithinAbs(0.0, 1.0e-13));
const double adjoint_lhs = gravity_gradient * expected_gradient_residual;
const double adjoint_rhs = gravity_potential * expected_poisson_residual;
const double adjoint_scale = std::max({std::abs(adjoint_lhs), std::abs(adjoint_rhs), 1.0e-14});
CHECK_THAT(std::abs(adjoint_lhs - adjoint_rhs) / adjoint_scale, WithinAbs(0.0, 1.0e-12));
}
TEST_CASE(
"Gravity Field Operator Mult Applies Stellar Source With Correct Sign",
tags::unit &tags::solver &tags::gravity &tags::mfem_operators
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
physics::update_stiffness_matrix(f);
REQUIRE(f.domainMapperStateless != nullptr);
const blocks::form_layout<form> layout = make_gravity_layout(f);
operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(
f, *f.domainMapperStateless
);
operators::GravityFieldJacobianOperator gravity_jacobian(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()
);
operators::GravityFieldOperator gravity_operator(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian
);
mfem::Vector state(layout.value_offsets().Last());
mfem::Vector residual;
state = 0.0;
constexpr operators::context::gravity_field::GravityFieldRevisions revisions{
.discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1}
};
const mfem::Vector density = make_constant_density(f, 1.0);
set_block(state, layout.value_offsets(), density_block, density);
gravity_operator.Prepare(state, revisions);
gravity_operator.Mult(state, residual);
const mfem::Vector gradient_residual =
get_block(residual, layout.residual_offsets(), gravity_gradient_residual_block);
const mfem::Vector poisson_residual =
get_block(residual, layout.residual_offsets(), gravity_poisson_residual_block);
CHECK_THAT(gradient_residual.Norml2(), WithinAbs(0.0, 1.0e-14));
CHECK(poisson_residual.Norml2() > 0.0);
const mfem::Vector constant_test = make_constant_density(f, 1.0);
const double integrated_source_residual = constant_test * poisson_residual;
INFO("Integrated Poisson source residual = " << integrated_source_residual);
CHECK(integrated_source_residual < 0.0);
mfem::Vector doubled_state(state);
mfem::Vector doubled_density(density);
doubled_density *= 2.0;
set_block(doubled_state, layout.value_offsets(), density_block, doubled_density);
mfem::Vector doubled_residual;
gravity_operator.Mult(doubled_state, doubled_residual);
mfem::Vector expected_doubled_residual(residual);
expected_doubled_residual *= 2.0;
CHECK_THAT(relative_difference(doubled_residual, expected_doubled_residual), WithinAbs(0.0, 1.0e-12));
}
TEST_CASE(
"Gravity Field Operator Mult Ignores Vacuum Density",
tags::unit &tags::solver &tags::gravity &tags::mfem_operators
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
physics::update_stiffness_matrix(f);
REQUIRE(f.domainMapperStateless != nullptr);
const blocks::form_layout<form> layout = make_gravity_layout(f);
operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(
f, *f.domainMapperStateless
);
operators::GravityFieldJacobianOperator gravity_jacobian(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()
);
operators::GravityFieldOperator gravity_operator(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian
);
mfem::Vector state(layout.value_offsets().Last());
mfem::Vector residual;
state = 0.0;
const operators::context::gravity_field::GravityFieldRevisions revisions{
.discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1}
};
const mfem::Vector vacuum_density = make_vacuum_density(f, 1.0);
REQUIRE(vacuum_density.Norml2() > 0.0);
set_block(state, layout.value_offsets(), density_block, vacuum_density);
gravity_operator.Prepare(state, revisions);
gravity_operator.Mult(state, residual);
INFO("Vacuum-source residual norm = " << residual.Norml2());
CHECK_THAT(residual.Norml2(), WithinAbs(0.0, 1.0e-13));
}
TEST_CASE(
"Gravity Field Operator Mult Produces A Symmetric Positive Hdiv Mass "
"Action",
tags::unit &tags::solver &tags::gravity &tags::mfem_operators
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
physics::update_stiffness_matrix(f);
REQUIRE(f.domainMapperStateless != nullptr);
const blocks::form_layout<form> layout = make_gravity_layout(f);
operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(
f, *f.domainMapperStateless
);
operators::GravityFieldJacobianOperator gravity_jacobian(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()
);
operators::GravityFieldOperator gravity_operator(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian
);
const mfem::Vector displacement = make_displacement(f);
const mfem::Vector gravity_gradient_a = make_test_vector(layout.size(gravity_gradient_block), 0.27);
const mfem::Vector gravity_gradient_b = make_test_vector(layout.size(gravity_gradient_block), 1.13);
mfem::Vector state_a(layout.value_offsets().Last());
mfem::Vector state_b(layout.value_offsets().Last());
mfem::Vector residual_a;
mfem::Vector residual_b;
state_a = 0.0;
state_b = 0.0;
set_block(state_a, layout.value_offsets(), displacement_block, displacement);
set_block(state_a, layout.value_offsets(), gravity_gradient_block, gravity_gradient_a);
set_block(state_b, layout.value_offsets(), displacement_block, displacement);
set_block(state_b, layout.value_offsets(), gravity_gradient_block, gravity_gradient_b);
const operators::context::gravity_field::GravityFieldRevisions revisions{
.discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1}
};
gravity_operator.Prepare(state_a, revisions);
gravity_operator.Mult(state_a, residual_a);
gravity_operator.Mult(state_b, residual_b);
const mfem::Vector mass_action_a =
get_block(residual_a, layout.residual_offsets(), gravity_gradient_residual_block);
const mfem::Vector mass_action_b =
get_block(residual_b, layout.residual_offsets(), gravity_gradient_residual_block);
const double energy_a = gravity_gradient_a * mass_action_a;
const double energy_b = gravity_gradient_b * mass_action_b;
const double cross_ab = gravity_gradient_a * mass_action_b;
const double cross_ba = gravity_gradient_b * mass_action_a;
const double symmetry_scale = std::max({std::abs(cross_ab), std::abs(cross_ba), 1.0e-14});
INFO("Mapped H(div) energy A = " << energy_a);
INFO("Mapped H(div) energy B = " << energy_b);
INFO("Mapped H(div) cross action A-M-B = " << cross_ab);
INFO("Mapped H(div) cross action B-M-A = " << cross_ba);
CHECK(std::isfinite(energy_a));
CHECK(std::isfinite(energy_b));
CHECK(energy_a > 0.0);
CHECK(energy_b > 0.0);
CHECK_THAT(std::abs(cross_ab - cross_ba) / symmetry_scale, WithinAbs(0.0, 1.0e-11));
}
TEST_CASE(
"Gravity Field Operator Mult Is Additive In Fields At Fixed Displacement",
tags::unit &tags::solver &tags::gravity &tags::mfem_operators
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
physics::update_stiffness_matrix(f);
REQUIRE(f.domainMapperStateless != nullptr);
const blocks::form_layout<form> layout = make_gravity_layout(f);
operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(
f, *f.domainMapperStateless
);
operators::GravityFieldJacobianOperator gravity_jacobian(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()
);
operators::GravityFieldOperator gravity_operator(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian
);
const mfem::Vector displacement = make_displacement(f);
const mfem::Vector density = make_constant_density(f, 0.73);
const mfem::Vector gravity_gradient = make_test_vector(layout.size(gravity_gradient_block), 0.41);
const mfem::Vector gravity_potential = make_test_vector(layout.size(gravity_potential_block), 0.89);
mfem::Vector displacement_state(layout.value_offsets().Last());
mfem::Vector density_state(layout.value_offsets().Last());
mfem::Vector gradient_state(layout.value_offsets().Last());
mfem::Vector potential_state(layout.value_offsets().Last());
mfem::Vector complete_state(layout.value_offsets().Last());
displacement_state = 0.0;
density_state = 0.0;
gradient_state = 0.0;
potential_state = 0.0;
complete_state = 0.0;
set_block(displacement_state, layout.value_offsets(), displacement_block, displacement);
set_block(density_state, layout.value_offsets(), displacement_block, displacement);
set_block(density_state, layout.value_offsets(), density_block, density);
set_block(gradient_state, layout.value_offsets(), displacement_block, displacement);
set_block(gradient_state, layout.value_offsets(), gravity_gradient_block, gravity_gradient);
set_block(potential_state, layout.value_offsets(), displacement_block, displacement);
set_block(potential_state, layout.value_offsets(), gravity_potential_block, gravity_potential);
set_block(complete_state, layout.value_offsets(), displacement_block, displacement);
set_block(complete_state, layout.value_offsets(), density_block, density);
set_block(complete_state, layout.value_offsets(), gravity_gradient_block, gravity_gradient);
set_block(complete_state, layout.value_offsets(), gravity_potential_block, gravity_potential);
mfem::Vector displacement_residual;
mfem::Vector density_residual;
mfem::Vector gradient_residual;
mfem::Vector potential_residual;
mfem::Vector complete_residual;
const operators::context::gravity_field::GravityFieldRevisions revisions{
.discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1}
};
gravity_operator.Prepare(complete_state, revisions);
gravity_operator.Mult(displacement_state, displacement_residual);
gravity_operator.Mult(density_state, density_residual);
gravity_operator.Mult(gradient_state, gradient_residual);
gravity_operator.Mult(potential_state, potential_residual);
gravity_operator.Mult(complete_state, complete_residual);
CHECK_THAT(displacement_residual.Norml2(), WithinAbs(0.0, 1.0e-14));
mfem::Vector additive_residual(density_residual);
additive_residual += gradient_residual;
additive_residual += potential_residual;
CHECK_THAT(relative_difference(complete_residual, additive_residual), WithinAbs(0.0, 1.0e-11));
}
TEST_CASE(
"Gravity Field Operator Stellar Hdiv Mass Matches Legacy Assembled Mass",
tags::integration &tags::solver &tags::gravity &tags::mfem_operators &tags::legacy_comparison
) {
constexpr double parity_tolerance = 1.0e-10;
constexpr double vacuum_tolerance = 1.0e-13;
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.mapping != nullptr);
REQUIRE(f.domainMapperStateless != nullptr);
const blocks::form_layout<form> layout = make_gravity_layout(f);
const mfem::Vector gravity_gradient = make_core_supported_gravity_gradient(f);
const mfem::Vector deformed_displacement = make_displacement(f);
MPI_Comm communicator = f.gravityFluxFes->GetComm();
REQUIRE(global_norm(gravity_gradient, communicator) > 0.0);
mfem::ParGridFunction local_gravity_gradient(f.gravityFluxFes.get());
local_gravity_gradient.SetFromTrueDofs(gravity_gradient);
double local_maximum_vacuum_dof = 0.0;
mfem::Array<int> element_vdofs;
mfem::Vector element_values;
for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) {
if (f.mesh->GetAttribute(element_id) != f.domainMapperStateless->GetVacuumElementAttribute())
continue;
f.gravityFluxFes->GetElementVDofs(element_id, element_vdofs);
local_gravity_gradient.GetSubVector(element_vdofs, element_values);
if (element_values.Size() > 0)
local_maximum_vacuum_dof = std::max(local_maximum_vacuum_dof, element_values.Normlinf());
}
double global_maximum_vacuum_dof = 0.0;
MPI_Allreduce(&local_maximum_vacuum_dof, &global_maximum_vacuum_dof, 1, MPI_DOUBLE, MPI_MAX, communicator);
INFO("Maximum gravity-gradient DOF on vacuum elements = " << global_maximum_vacuum_dof);
REQUIRE_THAT(global_maximum_vacuum_dof, WithinAbs(0.0, vacuum_tolerance));
for (const bool use_deformation : std::array{false, true}) {
DYNAMIC_SECTION("Geometry = " << (use_deformation ? "deformed" : "identity")) {
mfem::Vector displacement(layout.size(displacement_block));
displacement = 0.0;
if (use_deformation)
displacement = deformed_displacement;
mfem::ParGridFunction legacy_displacement(f.displacementFes.get());
legacy_displacement.SetFromTrueDofs(displacement);
f.mapping->SetDisplacement(legacy_displacement);
physics::update_stiffness_matrix(f);
REQUIRE(f.gravityContext.m_form != nullptr);
REQUIRE(f.gravityContext.b_form != nullptr);
REQUIRE(f.gravityContext.BT != nullptr);
operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(
f, *f.domainMapperStateless
);
operators::GravityFieldJacobianOperator gravity_jacobian(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()
);
operators::GravityFieldOperator gravity_operator(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian
);
mfem::Vector state(layout.value_offsets().Last());
mfem::Vector matrix_free_residual;
state = 0.0;
set_block(state, layout.value_offsets(), displacement_block, displacement);
set_block(state, layout.value_offsets(), gravity_gradient_block, gravity_gradient);
constexpr operators::context::gravity_field::GravityFieldRevisions revisions{
.discretization = {1},
.displacement = {1},
.density = {1},
.gravity_gradient = {1},
.gravity_potential = {1}
};
gravity_operator.Prepare(state, revisions);
gravity_operator.Mult(state, matrix_free_residual);
const mfem::Vector matrix_free_mass_action =
get_block(matrix_free_residual, layout.residual_offsets(), gravity_gradient_residual_block);
mfem::Vector legacy_mass_action(f.gravityFluxFes->GetTrueVSize());
f.gravityContext.m_form->Mult(gravity_gradient, legacy_mass_action);
const double matrix_free_mass_norm = global_norm(matrix_free_mass_action, communicator);
const double legacy_mass_norm = global_norm(legacy_mass_action, communicator);
const double mass_error =
global_relative_difference(matrix_free_mass_action, legacy_mass_action, communicator);
mfem::Vector mass_difference(matrix_free_mass_action);
mass_difference -= legacy_mass_action;
INFO("Geometry = " << (use_deformation ? "deformed" : "identity"));
INFO("Matrix-free stellar mass norm = " << matrix_free_mass_norm);
INFO("Legacy stellar mass norm = " << legacy_mass_norm);
INFO("Absolute stellar mass difference norm = " << global_norm(mass_difference, communicator));
INFO("Relative stellar mass-action error = " << mass_error);
REQUIRE(matrix_free_mass_norm > 0.0);
REQUIRE(legacy_mass_norm > 0.0);
CHECK_THAT(mass_error, WithinAbs(0.0, parity_tolerance));
}
}
}
TEST_CASE(
"Gravity Field Operator Hdiv Mass Action Matches Stateless Quadrature "
"Reference",
tags::integration &tags::solver &tags::gravity &tags::mfem_operators
) {
const utils::Args args = test_utils::setup_args();
fem::FEM fem = fem::setup_fem(args.mesh_file, args, 0);
fem.mapping->ResetDisplacement();
physics::update_stiffness_matrix(fem);
constexpr auto displacement_block = mean_field::utils::blocks::get_value_block<blocks::gravity_field_form>(
blocks::displacement_field.geometry_term
);
constexpr auto gravity_gradient_block =
mean_field::utils::blocks::get_value_block<blocks::gravity_field_form>(blocks::gravity_field.gradient_term);
constexpr auto gravity_gradient_residual_block =
mean_field::utils::blocks::get_residual_block<blocks::gravity_field_form>(blocks::gravity_field.gradient_term);
const blocks::form_layout<blocks::gravity_field_form> layout = make_gravity_layout(fem);
operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(
fem, *fem.domainMapperStateless
);
operators::GravityFieldJacobianOperator gravity_jacobian(
fem, *fem.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()
);
operators::GravityFieldOperator gravity_operator(
fem, *fem.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian
);
const MPI_Comm communicator = fem.gravityFluxFes->GetComm();
const mfem::Vector gravity_gradient_true = make_full_support_gravity_gradient(fem);
for (const bool deformed : {false, true}) {
DYNAMIC_SECTION("Geometry = " << (deformed ? "deformed" : "identity")) {
CAPTURE(deformed);
const mfem::Vector displacement_true = make_stateless_reference_displacement(fem, deformed);
mfem::Vector state(layout.value_offsets().Last());
state = 0.0;
for (int i = 0; i < gravity_gradient_true.Size(); ++i) {
state(layout.offset(gravity_gradient_block) + i) = gravity_gradient_true(i);
}
for (int i = 0; i < displacement_true.Size(); ++i) {
state(layout.offset(displacement_block) + i) = displacement_true(i);
}
const operators::context::gravity_field::GravityFieldRevisions revisions{
.discretization = {1},
.displacement = {1},
.density = {1},
.gravity_gradient = {1},
.gravity_potential = {1}
};
gravity_operator.Prepare(state, revisions);
mfem::Vector residual;
gravity_operator.Mult(state, residual);
mfem::Vector operator_action(layout.size(gravity_gradient_residual_block));
for (int i = 0; i < operator_action.Size(); ++i) {
operator_action(i) = residual(layout.offset(gravity_gradient_residual_block) + i);
}
const StatelessHDivMassReference reference =
evaluate_stateless_hdiv_mass_quadrature_reference(fem, gravity_gradient_true, displacement_true);
mfem::Vector decomposed_reference(reference.stellar_action);
decomposed_reference += reference.vacuum_action;
mfem::Vector action_difference(operator_action);
action_difference -= reference.total_action;
const double operator_norm = global_vector_norm(operator_action, communicator);
const double reference_norm = global_vector_norm(reference.total_action, communicator);
const double stellar_action_norm = global_vector_norm(reference.stellar_action, communicator);
const double vacuum_action_norm = global_vector_norm(reference.vacuum_action, communicator);
const double absolute_action_error = global_vector_norm(action_difference, communicator);
const double relative_action_error =
global_relative_vector_error(operator_action, reference.total_action, communicator);
const double decomposition_error =
global_relative_vector_error(reference.total_action, decomposed_reference, communicator);
const double operator_energy = global_vector_dot(gravity_gradient_true, operator_action, communicator);
const double reference_energy =
global_vector_dot(gravity_gradient_true, reference.total_action, communicator);
const double stellar_energy =
global_vector_dot(gravity_gradient_true, reference.stellar_action, communicator);
const double vacuum_energy =
global_vector_dot(gravity_gradient_true, reference.vacuum_action, communicator);
const double relative_energy_error =
std::abs(operator_energy - reference_energy) /
std::max(std::abs(reference_energy), std::numeric_limits<double>::epsilon());
const double vacuum_action_fraction =
vacuum_action_norm / std::max(reference_norm, std::numeric_limits<double>::epsilon());
const double vacuum_energy_fraction = vacuum_energy / reference_energy;
const double stellar_vacuum_dot =
global_vector_dot(reference.stellar_action, reference.vacuum_action, communicator);
const double stellar_vacuum_alignment =
stellar_vacuum_dot /
std::max(stellar_action_norm * vacuum_action_norm, std::numeric_limits<double>::epsilon());
INFO("Geometry = " << (deformed ? "deformed" : "identity"));
INFO("Stellar elements = " << reference.stellar_elements);
INFO("Vacuum elements = " << reference.vacuum_elements);
INFO("Stellar quadrature points = " << reference.stellar_quadrature_points);
INFO("Vacuum quadrature points = " << reference.vacuum_quadrature_points);
INFO(
"Stellar mapping determinant range = [" << reference.minimum_stellar_determinant << ", "
<< reference.maximum_stellar_determinant << "]"
);
INFO(
"Vacuum mapping determinant range = [" << reference.minimum_vacuum_determinant << ", "
<< reference.maximum_vacuum_determinant << "]"
);
INFO("Operator mass-action norm = " << operator_norm);
INFO("Stateless quadrature-reference norm = " << reference_norm);
INFO("Stellar action norm = " << stellar_action_norm);
INFO("Vacuum action norm = " << vacuum_action_norm);
INFO("Vacuum action fraction = " << vacuum_action_fraction);
INFO("Stellar-vacuum action alignment = " << stellar_vacuum_alignment);
INFO("Absolute action error = " << absolute_action_error);
INFO("Relative action error = " << relative_action_error);
INFO("Domain-decomposition error = " << decomposition_error);
INFO("Operator quadratic energy = " << operator_energy);
INFO("Quadrature-reference energy = " << reference_energy);
INFO("Stellar energy = " << stellar_energy);
INFO("Vacuum energy = " << vacuum_energy);
INFO("Vacuum energy fraction = " << vacuum_energy_fraction);
INFO("Relative energy error = " << relative_energy_error);
REQUIRE(reference.stellar_elements > 0);
REQUIRE(reference.vacuum_elements > 0);
REQUIRE(reference.stellar_quadrature_points > 0);
REQUIRE(reference.vacuum_quadrature_points > 0);
REQUIRE(reference.minimum_stellar_determinant > 0.0);
REQUIRE(reference.minimum_vacuum_determinant > 0.0);
REQUIRE(reference_energy > 0.0);
REQUIRE(stellar_energy > 0.0);
REQUIRE(vacuum_energy > 0.0);
constexpr double decomposition_tolerance = 5.0e-14;
constexpr double action_tolerance = 1.0e-11;
constexpr double energy_tolerance = 1.0e-11;
CHECK_THAT(decomposition_error, Catch::Matchers::WithinAbs(0.0, decomposition_tolerance));
CHECK_THAT(relative_action_error, Catch::Matchers::WithinAbs(0.0, action_tolerance));
CHECK_THAT(relative_energy_error, Catch::Matchers::WithinAbs(0.0, energy_tolerance));
}
}
}
TEST_CASE(
"Gravity Field Jacobian Fixed Geometry Blocks Match Centered Differences",
tags::integration &tags::solver &tags::gravity &tags::mfem_operators
) {
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 gravity_layout layout = make_gravity_jacobian_layout(f);
operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(
f, *f.domainMapperStateless
);
operators::GravityFieldJacobianOperator gravity_jacobian(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()
);
operators::GravityFieldOperator gravity_operator(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian
);
mfem::Vector state(layout.value_offsets().Last());
state = 0.0;
const mfem::Vector displacement = make_stateless_reference_displacement(f, true);
set_value_block(state, layout, displacement_block, displacement);
const operators::context::gravity_field::GravityFieldRevisions revisions{
.discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1}
};
gravity_operator.Prepare(state, revisions);
mfem::Operator &gradient = gravity_operator.GetGradient(state);
MPI_Comm communicator = f.mesh->GetComm();
struct DirectionCase {
std::string name;
mfem::Vector direction;
bool expect_gradient_residual;
bool expect_poisson_residual;
};
std::vector<DirectionCase> cases;
cases.push_back({"density", make_density_direction(layout), false, true});
cases.push_back({"gravity gradient", make_gravity_gradient_direction(layout), true, true});
cases.push_back({"gravity potential", make_gravity_potential_direction(layout), true, false});
for (const DirectionCase &direction_case : cases) {
DYNAMIC_SECTION("Direction = " << direction_case.name) {
constexpr double difference_step = 1.0e-6;
mfem::Vector jacobian_action;
gradient.Mult(direction_case.direction, jacobian_action);
const mfem::Vector finite_difference =
evaluate_centered_difference(gravity_operator, state, direction_case.direction, difference_step);
const mfem::Vector gradient_action =
get_residual_block(jacobian_action, layout, gravity_gradient_residual_block);
const mfem::Vector poisson_action =
get_residual_block(jacobian_action, layout, gravity_poisson_residual_block);
const double relative_error =
global_relative_vector_error(jacobian_action, finite_difference, communicator);
const double gradient_norm = global_vector_norm(gradient_action, communicator);
const double poisson_norm = global_vector_norm(poisson_action, communicator);
INFO("Direction = " << direction_case.name);
INFO("Jacobian action norm = " << global_vector_norm(jacobian_action, communicator));
INFO("Finite-difference action norm = " << global_vector_norm(finite_difference, communicator));
INFO("Gradient residual action norm = " << gradient_norm);
INFO("Poisson residual action norm = " << poisson_norm);
INFO("Relative Jacobian error = " << relative_error);
constexpr double jacobian_tolerance = 2.0e-8;
constexpr double zero_tolerance = 1.0e-13;
CHECK_THAT(relative_error, Catch::Matchers::WithinAbs(0.0, jacobian_tolerance));
if (direction_case.expect_gradient_residual) {
CHECK(gradient_norm > zero_tolerance);
} else {
CHECK_THAT(gradient_norm, Catch::Matchers::WithinAbs(0.0, zero_tolerance));
}
if (direction_case.expect_poisson_residual) {
CHECK(poisson_norm > zero_tolerance);
} else {
CHECK_THAT(poisson_norm, Catch::Matchers::WithinAbs(0.0, zero_tolerance));
}
}
}
}
TEST_CASE(
"Gravity Field Jacobian Combined Fixed Geometry Direction Matches Centered "
"Differences",
tags::integration &tags::solver &tags::gravity &tags::mfem_operators
) {
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 gravity_layout layout = make_gravity_jacobian_layout(f);
operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(
f, *f.domainMapperStateless
);
operators::GravityFieldJacobianOperator gravity_jacobian(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()
);
operators::GravityFieldOperator gravity_operator(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian
);
const mfem::Vector state = make_gravity_jacobian_state(f, layout, true);
const mfem::Vector direction = make_combined_fixed_geometry_direction(layout);
operators::context::gravity_field::GravityFieldRevisions revisions{
.discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1}
};
gravity_operator.Prepare(state, revisions);
mfem::Operator &gradient = gravity_operator.GetGradient(state);
mfem::Vector jacobian_action;
gradient.Mult(direction, jacobian_action);
MPI_Comm communicator = f.mesh->GetComm();
double best_error = std::numeric_limits<double>::infinity();
for (const double step : std::array{1.0e-4, 1.0e-6, 1.0e-7}) {
const mfem::Vector finite_difference = evaluate_centered_difference(gravity_operator, state, direction, step);
const double relative_error = global_relative_vector_error(jacobian_action, finite_difference, communicator);
best_error = std::min(best_error, relative_error);
INFO("Centered-difference step = " << step);
INFO("Relative Jacobian error = " << relative_error);
CHECK(relative_error < 2.0e-7);
}
INFO("Best relative Jacobian error = " << best_error);
CHECK(best_error < 2.0e-8);
}
TEST_CASE(
"Gravity Field Jacobian Action Is Linear At Fixed Geometry",
tags::unit &tags::solver &tags::gravity &tags::mfem_operators
) {
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 gravity_layout layout = make_gravity_jacobian_layout(f);
operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(
f, *f.domainMapperStateless
);
operators::GravityFieldJacobianOperator gravity_jacobian(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()
);
operators::GravityFieldOperator gravity_operator(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian
);
const mfem::Vector state = make_gravity_jacobian_state(f, layout, false);
const mfem::Vector direction_a = make_density_direction(layout);
const mfem::Vector direction_b = make_gravity_gradient_direction(layout);
const operators::context::gravity_field::GravityFieldRevisions revisions{
.discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1}
};
gravity_operator.Prepare(state, revisions);
mfem::Operator &gradient = gravity_operator.GetGradient(state);
constexpr double scale_a = 1.7;
constexpr double scale_b = -0.6;
mfem::Vector combined_direction(direction_a);
combined_direction *= scale_a;
combined_direction.Add(scale_b, direction_b);
mfem::Vector action_a;
mfem::Vector action_b;
mfem::Vector combined_action;
gradient.Mult(direction_a, action_a);
gradient.Mult(direction_b, action_b);
gradient.Mult(combined_direction, combined_action);
mfem::Vector expected_action(action_a);
expected_action *= scale_a;
expected_action.Add(scale_b, action_b);
MPI_Comm communicator = f.mesh->GetComm();
const double relative_error = global_relative_vector_error(combined_action, expected_action, communicator);
INFO("Relative Jacobian linearity error = " << relative_error);
CHECK_THAT(relative_error, Catch::Matchers::WithinAbs(0.0, 2.0e-13));
}
TEST_CASE(
"Gravity Field Prepare Updates Shared Linearization Context",
tags::unit &tags::solver &tags::gravity &tags::mfem_operators
) {
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 gravity_layout layout = make_gravity_jacobian_layout(f);
operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(
f, *f.domainMapperStateless
);
operators::GravityFieldJacobianOperator gravity_jacobian(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()
);
operators::GravityFieldOperator gravity_operator(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian
);
mfem::Vector state_a = make_gravity_jacobian_state(f, layout, false);
const mfem::Vector state_b = make_gravity_jacobian_state(f, layout, true);
const operators::context::gravity_field::GravityFieldRevisions revisions_a{
.discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1}
};
const operators::context::gravity_field::GravityFieldRevisions revisions_b{
.discretization = {1}, .displacement = {2}, .density = {2}, .gravity_gradient = {2}, .gravity_potential = {2}
};
MPI_Comm communicator = f.mesh->GetComm();
gravity_operator.Prepare(state_a, revisions_a);
mfem::Operator &returned_gradient_a = gravity_operator.GetGradient(state_a);
REQUIRE(&returned_gradient_a == static_cast<mfem::Operator *>(&gravity_jacobian));
check_linearization_context_matches_state(linearization_context, state_a, layout.value_offsets(), communicator);
state_a = 0.0;
CHECK(linearization_context.IsPrepared());
const double stored_state_norm =
global_vector_norm(linearization_context.GetDensity(), communicator) +
global_vector_norm(linearization_context.GetGeometryContext().GetDisplacement(), communicator) +
global_vector_norm(linearization_context.GetGravityGradient(), communicator);
CHECK(stored_state_norm > 0.0);
gravity_operator.Prepare(state_b, revisions_b);
mfem::Operator &returned_gradient_b = gravity_operator.GetGradient(state_b);
REQUIRE(&returned_gradient_b == static_cast<mfem::Operator *>(&gravity_jacobian));
check_linearization_context_matches_state(linearization_context, state_b, layout.value_offsets(), communicator);
}
TEST_CASE(
"Mapped Hdiv Mass Variation Is Linear In Displacement Direction",
tags::integration &tags::solver &tags::gravity &tags::mfem_operators
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.domainMapperStateless != nullptr);
const mapping::DomainMapperStateless &domain_mapper = *f.domainMapperStateless;
const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f);
MPI_Comm communicator = f.gravityFluxFes->GetComm();
mfem::Vector zero_direction(f.displacementFes->GetTrueVSize());
mfem::Vector zero_gravity_gradient(f.gravityFluxFes->GetTrueVSize());
zero_direction = 0.0;
zero_gravity_gradient = 0.0;
mfem::Vector zero_direction_action;
operators::kernels::apply_mapped_hdiv_mass_variation(
f, domain_mapper, fields.gravity_gradient, fields.displacement, zero_direction, zero_direction_action
);
INFO("Zero-direction action norm = " << global_norm(zero_direction_action, communicator));
CHECK(global_norm(zero_direction_action, communicator) < 1.0e-13);
mfem::Vector zero_gravity_action;
operators::kernels::apply_mapped_hdiv_mass_variation(
f, domain_mapper, zero_gravity_gradient, fields.displacement, fields.displacement_direction_1,
zero_gravity_action
);
INFO("Zero-gravity action norm = " << global_norm(zero_gravity_action, communicator));
CHECK(global_norm(zero_gravity_action, communicator) < 1.0e-13);
constexpr double scale_1 = 1.7;
constexpr double scale_2 = -0.6;
mfem::Vector combined_direction(fields.displacement_direction_1);
combined_direction *= scale_1;
combined_direction.Add(scale_2, fields.displacement_direction_2);
mfem::Vector action_1;
mfem::Vector action_2;
mfem::Vector combined_action;
operators::kernels::apply_mapped_hdiv_mass_variation(
f, domain_mapper, fields.gravity_gradient, fields.displacement, fields.displacement_direction_1, action_1
);
operators::kernels::apply_mapped_hdiv_mass_variation(
f, domain_mapper, fields.gravity_gradient, fields.displacement, fields.displacement_direction_2, action_2
);
operators::kernels::apply_mapped_hdiv_mass_variation(
f, domain_mapper, fields.gravity_gradient, fields.displacement, combined_direction, combined_action
);
mfem::Vector expected_action(action_1);
expected_action *= scale_1;
expected_action.Add(scale_2, action_2);
const double linearity_error = global_relative_error(combined_action, expected_action, communicator);
INFO("Combined action norm = " << global_norm(combined_action, communicator));
INFO("Expected action norm = " << global_norm(expected_action, communicator));
INFO("Displacement-direction linearity error = " << linearity_error);
CHECK_THAT(linearity_error, Catch::Matchers::WithinAbs(0.0, 2.0e-12));
}
TEST_CASE(
"Mapped Hdiv Mass Variation Matches Centered Geometry Differences",
tags::integration &tags::solver &tags::gravity &tags::mfem_operators
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.domainMapperStateless != nullptr);
const mapping::DomainMapperStateless &domain_mapper = *f.domainMapperStateless;
const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f);
MPI_Comm communicator = f.gravityFluxFes->GetComm();
constexpr double difference_step = 1.0e-5;
constexpr double comparison_tolerance = 2.0e-7;
mfem::Vector identity_displacement(f.displacementFes->GetTrueVSize());
identity_displacement = 0.0;
for (const bool deformed : std::array{false, true}) {
const mfem::Vector &base_displacement = deformed ? fields.displacement : identity_displacement;
mfem::Vector analytic_action;
operators::kernels::apply_mapped_hdiv_mass_variation(
f, domain_mapper, fields.gravity_gradient, base_displacement, fields.displacement_direction_1,
analytic_action
);
const mfem::Vector finite_difference_action = centered_hdiv_mass_geometry_difference(
f, domain_mapper, fields.gravity_gradient, base_displacement, fields.displacement_direction_1,
difference_step
);
const double relative_error = global_relative_error(analytic_action, finite_difference_action, communicator);
INFO("Geometry = " << (deformed ? "deformed" : "identity"));
INFO("Analytic variation norm = " << global_norm(analytic_action, communicator));
INFO("Finite-difference variation norm = " << global_norm(finite_difference_action, communicator));
INFO("Relative H(div) mass-variation error = " << relative_error);
CHECK_THAT(relative_error, Catch::Matchers::WithinAbs(0.0, comparison_tolerance));
}
}
TEST_CASE(
"Mapped Hdiv Mass Geometry Difference Converges To Analytic Variation",
tags::integration &tags::solver &tags::gravity &tags::mfem_operators &tags::convergence
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.domainMapperStateless != nullptr);
const mapping::DomainMapperStateless &domain_mapper = *f.domainMapperStateless;
const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f);
MPI_Comm communicator = f.gravityFluxFes->GetComm();
mfem::Vector analytic_action;
operators::kernels::apply_mapped_hdiv_mass_variation(
f, domain_mapper, fields.gravity_gradient, fields.displacement, fields.displacement_direction_1, analytic_action
);
constexpr std::array<double, 3> difference_steps{8.0e-2, 4.0e-2, 2.0e-2};
std::array<double, difference_steps.size()> errors{};
for (std::size_t i = 0; i < difference_steps.size(); ++i) {
const mfem::Vector finite_difference_action = centered_hdiv_mass_geometry_difference(
f, domain_mapper, fields.gravity_gradient, fields.displacement, fields.displacement_direction_1,
difference_steps[i]
);
errors[i] = global_relative_error(finite_difference_action, analytic_action, communicator);
INFO("Difference step = " << difference_steps[i]);
INFO("Relative error = " << errors[i]);
}
const double first_observed_order = std::log(errors[0] / errors[1]) / std::log(2.0);
const double second_observed_order = std::log(errors[1] / errors[2]) / std::log(2.0);
INFO("Errors = [" << errors[0] << ", " << errors[1] << ", " << errors[2] << "]");
INFO("First observed convergence order = " << first_observed_order);
INFO("Second observed convergence order = " << second_observed_order);
CHECK(errors[1] < errors[0]);
CHECK(errors[2] < errors[1]);
CHECK(first_observed_order > 1.8);
CHECK(second_observed_order > 1.8);
}
TEST_CASE(
"Mapped Source Variation Is Linear In Displacement Direction",
tags::integration &tags::solver &tags::gravity &tags::mfem_operators
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.domainMapperStateless != nullptr);
const mapping::DomainMapperStateless &domain_mapper = *f.domainMapperStateless;
const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f);
const mfem::Vector density = make_source_variation_density(f);
MPI_Comm communicator = f.mesh->GetComm();
mfem::Vector zero_density(f.densityFes->GetTrueVSize());
mfem::Vector zero_direction(f.displacementFes->GetTrueVSize());
zero_density = 0.0;
zero_direction = 0.0;
mfem::Vector zero_direction_action;
operators::kernels::apply_mapped_source_variation(
f, domain_mapper, density, fields.displacement, zero_direction, zero_direction_action
);
REQUIRE(zero_direction_action.Size() == f.gravityPotentialFes->GetTrueVSize());
CHECK(global_norm(zero_direction_action, communicator) < 1.0e-13);
mfem::Vector zero_density_action;
operators::kernels::apply_mapped_source_variation(
f, domain_mapper, zero_density, fields.displacement, fields.displacement_direction_1, zero_density_action
);
REQUIRE(zero_density_action.Size() == f.gravityPotentialFes->GetTrueVSize());
CHECK(global_norm(zero_density_action, communicator) < 1.0e-13);
constexpr double scale_1 = 1.4;
constexpr double scale_2 = -0.7;
mfem::Vector combined_direction(fields.displacement_direction_1);
combined_direction *= scale_1;
combined_direction.Add(scale_2, fields.displacement_direction_2);
mfem::Vector action_1;
mfem::Vector action_2;
mfem::Vector combined_action;
operators::kernels::apply_mapped_source_variation(
f, domain_mapper, density, fields.displacement, fields.displacement_direction_1, action_1
);
operators::kernels::apply_mapped_source_variation(
f, domain_mapper, density, fields.displacement, fields.displacement_direction_2, action_2
);
operators::kernels::apply_mapped_source_variation(
f, domain_mapper, density, fields.displacement, combined_direction, combined_action
);
REQUIRE(action_1.Size() == f.gravityPotentialFes->GetTrueVSize());
REQUIRE(action_2.Size() == f.gravityPotentialFes->GetTrueVSize());
REQUIRE(combined_action.Size() == f.gravityPotentialFes->GetTrueVSize());
mfem::Vector expected_action(action_1);
expected_action *= scale_1;
expected_action.Add(scale_2, action_2);
const double linearity_error = global_relative_error(combined_action, expected_action, communicator);
INFO("Combined source-variation norm = " << global_norm(combined_action, communicator));
INFO("Expected source-variation norm = " << global_norm(expected_action, communicator));
INFO("Source-variation linearity error = " << linearity_error);
CHECK_THAT(linearity_error, Catch::Matchers::WithinAbs(0.0, 2.0e-12));
}
TEST_CASE(
"Mapped Source Variation Matches Centered Geometry Differences",
tags::integration &tags::solver &tags::gravity &tags::mfem_operators
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.domainMapperStateless != nullptr);
const mapping::DomainMapperStateless &domain_mapper = *f.domainMapperStateless;
const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f);
const mfem::Vector density = make_source_variation_density(f);
MPI_Comm communicator = f.mesh->GetComm();
constexpr double difference_step = 1.0e-5;
constexpr double comparison_tolerance = 2.0e-7;
mfem::Vector identity_displacement(f.displacementFes->GetTrueVSize());
identity_displacement = 0.0;
for (const bool deformed : std::array{false, true}) {
const mfem::Vector &base_displacement = deformed ? fields.displacement : identity_displacement;
mfem::Vector analytic_action;
operators::kernels::apply_mapped_source_variation(
f, domain_mapper, density, base_displacement, fields.displacement_direction_1, analytic_action
);
const mfem::Vector finite_difference_action = centered_source_geometry_difference(
f, domain_mapper, density, base_displacement, fields.displacement_direction_1, difference_step
);
REQUIRE(analytic_action.Size() == f.gravityPotentialFes->GetTrueVSize());
REQUIRE(finite_difference_action.Size() == f.gravityPotentialFes->GetTrueVSize());
const double relative_error = global_relative_error(analytic_action, finite_difference_action, communicator);
INFO("Geometry = " << (deformed ? "deformed" : "identity"));
INFO("Analytic source-variation norm = " << global_norm(analytic_action, communicator));
INFO("Finite-difference source-variation norm = " << global_norm(finite_difference_action, communicator));
INFO("Relative source-variation error = " << relative_error);
CHECK_THAT(relative_error, Catch::Matchers::WithinAbs(0.0, comparison_tolerance));
}
}
TEST_CASE(
"Mapped Source Geometry Difference Converges To Analytic Variation",
tags::integration &tags::solver &tags::gravity &tags::mfem_operators &tags::convergence
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.domainMapperStateless != nullptr);
const mapping::DomainMapperStateless &domain_mapper = *f.domainMapperStateless;
const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f);
const mfem::Vector density = make_source_variation_density(f);
MPI_Comm communicator = f.mesh->GetComm();
mfem::Vector analytic_action;
operators::kernels::apply_mapped_source_variation(
f, domain_mapper, density, fields.displacement, fields.displacement_direction_1, analytic_action
);
REQUIRE(analytic_action.Size() == f.gravityPotentialFes->GetTrueVSize());
constexpr std::array<double, 3> difference_steps{8.0e-2, 4.0e-2, 2.0e-2};
std::array<double, difference_steps.size()> errors{};
for (std::size_t i = 0; i < difference_steps.size(); ++i) {
const mfem::Vector finite_difference_action = centered_source_geometry_difference(
f, domain_mapper, density, fields.displacement, fields.displacement_direction_1, difference_steps[i]
);
REQUIRE(finite_difference_action.Size() == f.gravityPotentialFes->GetTrueVSize());
errors[i] = global_relative_error(finite_difference_action, analytic_action, communicator);
}
const double first_observed_order = std::log(errors[0] / errors[1]) / std::log(2.0);
const double second_observed_order = std::log(errors[1] / errors[2]) / std::log(2.0);
INFO("Errors = [" << errors[0] << ", " << errors[1] << ", " << errors[2] << "]");
INFO("First observed convergence order = " << first_observed_order);
INFO("Second observed convergence order = " << second_observed_order);
CHECK(errors[1] < errors[0]);
CHECK(errors[2] < errors[1]);
CHECK(first_observed_order > 1.8);
CHECK(second_observed_order > 1.8);
}
TEST_CASE(
"Mapped Hdiv Mass Variation Is Symmetric",
tags::integration &tags::solver &tags::gravity &tags::mfem_operators
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.domainMapperStateless != nullptr);
const mapping::DomainMapperStateless &domain_mapper = *f.domainMapperStateless;
const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f);
const mfem::Vector second_gravity_gradient = make_secondary_gravity_gradient(f);
MPI_Comm communicator = f.gravityFluxFes->GetComm();
mfem::Vector identity_displacement(f.displacementFes->GetTrueVSize());
identity_displacement = 0.0;
for (const bool deformed : std::array{false, true}) {
const mfem::Vector &base_displacement = deformed ? fields.displacement : identity_displacement;
mfem::Vector first_action;
mfem::Vector second_action;
operators::kernels::apply_mapped_hdiv_mass_variation(
f, domain_mapper, fields.gravity_gradient, base_displacement, fields.displacement_direction_1, first_action
);
operators::kernels::apply_mapped_hdiv_mass_variation(
f, domain_mapper, second_gravity_gradient, base_displacement, fields.displacement_direction_1, second_action
);
REQUIRE(first_action.Size() == f.gravityFluxFes->GetTrueVSize());
REQUIRE(second_action.Size() == f.gravityFluxFes->GetTrueVSize());
const double left_pairing = global_dot(fields.gravity_gradient, second_action, communicator);
const double right_pairing = global_dot(second_gravity_gradient, first_action, communicator);
const double symmetry_error = std::abs(left_pairing - right_pairing) /
std::max({std::abs(left_pairing), std::abs(right_pairing), 1.0e-14});
INFO("Geometry = " << (deformed ? "deformed" : "identity"));
INFO("g1^T delta_M g2 = " << left_pairing);
INFO("g2^T delta_M g1 = " << right_pairing);
INFO("Relative symmetry error = " << symmetry_error);
CHECK_THAT(symmetry_error, Catch::Matchers::WithinAbs(0.0, 1.0e-11));
}
}
TEST_CASE(
"Mapped Geometry Variations Are Linear In Base Fields",
tags::integration &tags::solver &tags::gravity &tags::mfem_operators
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.domainMapperStateless != nullptr);
const mapping::DomainMapperStateless &domain_mapper = *f.domainMapperStateless;
const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f);
const mfem::Vector second_gravity_gradient = make_secondary_gravity_gradient(f);
const mfem::Vector first_density = make_source_variation_density(f);
const mfem::Vector second_density = make_secondary_source_density(f);
constexpr double scale_1 = 1.3;
constexpr double scale_2 = -0.4;
{
MPI_Comm communicator = f.gravityFluxFes->GetComm();
mfem::Vector combined_gravity_gradient(fields.gravity_gradient);
combined_gravity_gradient *= scale_1;
combined_gravity_gradient.Add(scale_2, second_gravity_gradient);
mfem::Vector first_action;
mfem::Vector second_action;
mfem::Vector combined_action;
operators::kernels::apply_mapped_hdiv_mass_variation(
f, domain_mapper, fields.gravity_gradient, fields.displacement, fields.displacement_direction_1,
first_action
);
operators::kernels::apply_mapped_hdiv_mass_variation(
f, domain_mapper, second_gravity_gradient, fields.displacement, fields.displacement_direction_1,
second_action
);
operators::kernels::apply_mapped_hdiv_mass_variation(
f, domain_mapper, combined_gravity_gradient, fields.displacement, fields.displacement_direction_1,
combined_action
);
REQUIRE(first_action.Size() == f.gravityFluxFes->GetTrueVSize());
REQUIRE(second_action.Size() == f.gravityFluxFes->GetTrueVSize());
REQUIRE(combined_action.Size() == f.gravityFluxFes->GetTrueVSize());
mfem::Vector expected_action(first_action);
expected_action *= scale_1;
expected_action.Add(scale_2, second_action);
const double linearity_error = global_relative_error(combined_action, expected_action, communicator);
INFO("H(div) base-field linearity error = " << linearity_error);
CHECK_THAT(linearity_error, Catch::Matchers::WithinAbs(0.0, 3.0e-12));
}
{
MPI_Comm communicator = f.mesh->GetComm();
mfem::Vector combined_density(first_density);
combined_density *= scale_1;
combined_density.Add(scale_2, second_density);
mfem::Vector first_action;
mfem::Vector second_action;
mfem::Vector combined_action;
operators::kernels::apply_mapped_source_variation(
f, domain_mapper, first_density, fields.displacement, fields.displacement_direction_1, first_action
);
operators::kernels::apply_mapped_source_variation(
f, domain_mapper, second_density, fields.displacement, fields.displacement_direction_1, second_action
);
operators::kernels::apply_mapped_source_variation(
f, domain_mapper, combined_density, fields.displacement, fields.displacement_direction_1, combined_action
);
REQUIRE(first_action.Size() == f.gravityPotentialFes->GetTrueVSize());
REQUIRE(second_action.Size() == f.gravityPotentialFes->GetTrueVSize());
REQUIRE(combined_action.Size() == f.gravityPotentialFes->GetTrueVSize());
mfem::Vector expected_action(first_action);
expected_action *= scale_1;
expected_action.Add(scale_2, second_action);
const double linearity_error = global_relative_error(combined_action, expected_action, communicator);
INFO("Source base-field linearity error = " << linearity_error);
CHECK_THAT(linearity_error, Catch::Matchers::WithinAbs(0.0, 3.0e-12));
}
}
TEST_CASE(
"Mapped Source Variation Ignores Vacuum Density",
tags::integration &tags::solver &tags::gravity &tags::mfem_operators
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.domainMapperStateless != nullptr);
const mapping::DomainMapperStateless &domain_mapper = *f.domainMapperStateless;
const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f);
const int vacuum_attribute = domain_mapper.GetVacuumElementAttribute();
const mfem::Vector vacuum_density = make_vacuum_only_density(f, vacuum_attribute);
MPI_Comm communicator = f.mesh->GetComm();
const double vacuum_density_norm = global_norm(vacuum_density, communicator);
REQUIRE(vacuum_density_norm > 0.0);
mfem::Vector action;
operators::kernels::apply_mapped_source_variation(
f, domain_mapper, vacuum_density, fields.displacement, fields.displacement_direction_1, action
);
REQUIRE(action.Size() == f.gravityPotentialFes->GetTrueVSize());
const double action_norm = global_norm(action, communicator);
INFO("Vacuum density norm = " << vacuum_density_norm);
INFO("Source-variation action norm = " << action_norm);
CHECK_THAT(action_norm, Catch::Matchers::WithinAbs(0.0, 1.0e-14));
}
TEST_CASE(
"Gravity Field Jacobian Displacement Blocks Match Geometry Kernels",
tags::integration &tags::solver &tags::gravity &tags::mfem_operators
) {
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);
REQUIRE(f.domainMapperStateless != nullptr);
const gravity_layout layout = make_gravity_jacobian_layout(f);
const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f);
const mfem::Vector state = make_gravity_jacobian_state(f, layout, true);
const mfem::Vector direction = make_displacement_direction(layout, fields.displacement_direction_1);
const mfem::Vector density = get_value_block(state, layout, density_block);
const mfem::Vector displacement = get_value_block(state, layout, displacement_block);
const mfem::Vector gravity_gradient = get_value_block(state, layout, gravity_gradient_block);
operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(
f, *f.domainMapperStateless
);
operators::GravityFieldJacobianOperator gravity_jacobian(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()
);
operators::GravityFieldOperator gravity_operator(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian
);
const operators::context::gravity_field::GravityFieldRevisions revisions{
.discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1}
};
gravity_operator.Prepare(state, revisions);
mfem::Vector jacobian_action;
gravity_jacobian.Mult(direction, jacobian_action);
REQUIRE(jacobian_action.Size() == layout.residual_offsets().Last());
const mfem::Vector gradient_action = get_residual_block(jacobian_action, layout, gravity_gradient_residual_block);
const mfem::Vector poisson_action = get_residual_block(jacobian_action, layout, gravity_poisson_residual_block);
mfem::Vector expected_gradient_action;
mfem::Vector expected_poisson_action;
operators::kernels::apply_mapped_hdiv_mass_variation(
f, *f.domainMapperStateless, gravity_gradient, displacement, fields.displacement_direction_1,
expected_gradient_action
);
operators::kernels::apply_mapped_source_variation(
f, *f.domainMapperStateless, density, displacement, fields.displacement_direction_1, expected_poisson_action
);
expected_poisson_action *= -1.0;
MPI_Comm communicator = f.mesh->GetComm();
const double gradient_error = global_relative_error(gradient_action, expected_gradient_action, communicator);
const double poisson_error = global_relative_error(poisson_action, expected_poisson_action, communicator);
INFO("Gradient displacement action norm = " << global_norm(gradient_action, communicator));
INFO("Poisson displacement action norm = " << global_norm(poisson_action, communicator));
INFO("Gradient displacement-block error = " << gradient_error);
INFO("Poisson displacement-block error = " << poisson_error);
REQUIRE(global_norm(gradient_action, communicator) > 1.0e-12);
REQUIRE(global_norm(poisson_action, communicator) > 1.0e-12);
CHECK_THAT(gradient_error, WithinAbs(0.0, 2.0e-12));
CHECK_THAT(poisson_error, WithinAbs(0.0, 2.0e-12));
}
TEST_CASE(
"Gravity Field Jacobian Displacement Direction Matches Centered "
"Differences",
tags::integration &tags::solver &tags::gravity &tags::mfem_operators
) {
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);
REQUIRE(f.domainMapperStateless != nullptr);
const gravity_layout layout = make_gravity_jacobian_layout(f);
const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f);
const mfem::Vector direction = make_displacement_direction(layout, fields.displacement_direction_1);
operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(
f, *f.domainMapperStateless
);
operators::GravityFieldJacobianOperator gravity_jacobian(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()
);
operators::GravityFieldOperator gravity_operator(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian
);
constexpr double difference_step = 1.0e-5;
constexpr double comparison_tolerance = 3.0e-7;
constexpr double nonzero_tolerance = 1.0e-12;
MPI_Comm communicator = f.mesh->GetComm();
REQUIRE(direction.Size() == layout.value_offsets().Last());
REQUIRE(global_vector_norm(direction, communicator) > nonzero_tolerance);
for (const bool deformed : std::array{false, true}) {
DYNAMIC_SECTION("Geometry = " << (deformed ? "deformed" : "identity")) {
const mfem::Vector state = make_gravity_jacobian_state(f, layout, deformed);
REQUIRE(state.Size() == layout.value_offsets().Last());
const operators::context::gravity_field::GravityFieldRevisions base_revisions{
.discretization = {1},
.displacement = {1},
.density = {1},
.gravity_gradient = {1},
.gravity_potential = {1}
};
gravity_operator.Prepare(state, base_revisions);
REQUIRE(linearization_context.IsPrepared());
check_linearization_context_matches_state(
linearization_context, state, layout.value_offsets(), communicator
);
const double base_density_norm = global_vector_norm(linearization_context.GetDensity(), communicator);
const double base_gravity_gradient_norm =
global_vector_norm(linearization_context.GetGravityGradient(), communicator);
INFO("Base density norm = " << base_density_norm);
INFO("Base gravity-gradient norm = " << base_gravity_gradient_norm);
REQUIRE(base_density_norm > nonzero_tolerance);
REQUIRE(base_gravity_gradient_norm > nonzero_tolerance);
mfem::Operator &gradient = gravity_operator.GetGradient(state);
REQUIRE(&gradient == static_cast<mfem::Operator *>(&gravity_jacobian));
mfem::Vector jacobian_action;
gradient.Mult(direction, jacobian_action);
REQUIRE(jacobian_action.Size() == layout.residual_offsets().Last());
mfem::Vector plus_state(state);
mfem::Vector minus_state(state);
plus_state.Add(difference_step, direction);
minus_state.Add(-difference_step, direction);
auto plus_revisions = base_revisions;
plus_revisions.displacement.value++;
mfem::Vector plus_residual;
gravity_operator.Prepare(plus_state, plus_revisions);
gravity_operator.Mult(plus_state, plus_residual);
REQUIRE(plus_residual.Size() == layout.residual_offsets().Last());
auto minus_revisions = plus_revisions;
minus_revisions.displacement.value++;
mfem::Vector minus_residual;
gravity_operator.Prepare(minus_state, minus_revisions);
gravity_operator.Mult(minus_state, minus_residual);
REQUIRE(minus_residual.Size() == layout.residual_offsets().Last());
mfem::Vector finite_difference(plus_residual);
finite_difference -= minus_residual;
finite_difference /= 2.0 * difference_step;
const mfem::Vector gradient_action =
get_residual_block(jacobian_action, layout, gravity_gradient_residual_block);
const mfem::Vector poisson_action =
get_residual_block(jacobian_action, layout, gravity_poisson_residual_block);
const mfem::Vector finite_difference_gradient =
get_residual_block(finite_difference, layout, gravity_gradient_residual_block);
const mfem::Vector finite_difference_poisson =
get_residual_block(finite_difference, layout, gravity_poisson_residual_block);
const double jacobian_gradient_norm = global_vector_norm(gradient_action, communicator);
const double jacobian_poisson_norm = global_vector_norm(poisson_action, communicator);
const double finite_difference_gradient_norm = global_vector_norm(finite_difference_gradient, communicator);
const double finite_difference_poisson_norm = global_vector_norm(finite_difference_poisson, communicator);
REQUIRE(jacobian_gradient_norm > nonzero_tolerance);
REQUIRE(jacobian_poisson_norm > nonzero_tolerance);
REQUIRE(finite_difference_gradient_norm > nonzero_tolerance);
REQUIRE(finite_difference_poisson_norm > nonzero_tolerance);
mfem::Vector gradient_difference(gradient_action);
gradient_difference -= finite_difference_gradient;
mfem::Vector poisson_difference(poisson_action);
poisson_difference -= finite_difference_poisson;
mfem::Vector combined_difference(jacobian_action);
combined_difference -= finite_difference;
const double absolute_gradient_error = global_vector_norm(gradient_difference, communicator);
const double absolute_poisson_error = global_vector_norm(poisson_difference, communicator);
const double absolute_combined_error = global_vector_norm(combined_difference, communicator);
const double gradient_error =
global_relative_error(gradient_action, finite_difference_gradient, communicator);
const double poisson_error = global_relative_error(poisson_action, finite_difference_poisson, communicator);
const double combined_error = global_relative_error(jacobian_action, finite_difference, communicator);
INFO("Geometry = " << (deformed ? "deformed" : "identity"));
INFO("Jacobian gradient action norm = " << jacobian_gradient_norm);
INFO("Finite-difference gradient norm = " << finite_difference_gradient_norm);
INFO("Jacobian Poisson action norm = " << jacobian_poisson_norm);
INFO("Finite-difference Poisson norm = " << finite_difference_poisson_norm);
INFO("Absolute gradient residual error = " << absolute_gradient_error);
INFO("Absolute Poisson residual error = " << absolute_poisson_error);
INFO("Absolute combined residual error = " << absolute_combined_error);
INFO("Gradient residual relative error = " << gradient_error);
INFO("Poisson residual relative error = " << poisson_error);
INFO("Combined residual relative error = " << combined_error);
REQUIRE(std::isfinite(gradient_error));
REQUIRE(std::isfinite(poisson_error));
REQUIRE(std::isfinite(combined_error));
CHECK_THAT(gradient_error, WithinAbs(0.0, comparison_tolerance));
CHECK_THAT(poisson_error, WithinAbs(0.0, comparison_tolerance));
CHECK_THAT(combined_error, WithinAbs(0.0, comparison_tolerance));
}
}
}
TEST_CASE(
"Gravity Field Jacobian Displacement Difference Converges At Second Order",
tags::integration &tags::solver &tags::gravity &tags::mfem_operators &tags::convergence
) {
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);
REQUIRE(f.domainMapperStateless != nullptr);
const gravity_layout layout = make_gravity_jacobian_layout(f);
const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f);
const mfem::Vector state = make_gravity_jacobian_state(f, layout, true);
const mfem::Vector direction = make_displacement_direction(layout, fields.displacement_direction_1);
operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(
f, *f.domainMapperStateless
);
operators::GravityFieldJacobianOperator gravity_jacobian(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()
);
operators::GravityFieldOperator gravity_operator(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian
);
operators::context::gravity_field::GravityFieldRevisions revisions{
.discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1}
};
MPI_Comm communicator = f.mesh->GetComm();
gravity_operator.Prepare(state, revisions);
REQUIRE(linearization_context.IsPrepared());
check_linearization_context_matches_state(linearization_context, state, layout.value_offsets(), communicator);
mfem::Operator &gradient = gravity_operator.GetGradient(state);
REQUIRE(&gradient == static_cast<mfem::Operator *>(&gravity_jacobian));
mfem::Vector jacobian_action;
gradient.Mult(direction, jacobian_action);
REQUIRE(jacobian_action.Size() == layout.residual_offsets().Last());
const double jacobian_action_norm = global_vector_norm(jacobian_action, communicator);
INFO("Jacobian action norm = " << jacobian_action_norm);
REQUIRE(jacobian_action_norm > 1.0e-12);
auto evaluate_geometry_centered_difference = [&](const double difference_step) {
mfem::Vector plus_state(state);
mfem::Vector minus_state(state);
plus_state.Add(difference_step, direction);
minus_state.Add(-difference_step, direction);
revisions.displacement.value++;
mfem::Vector plus_residual;
gravity_operator.Prepare(plus_state, revisions);
gravity_operator.Mult(plus_state, plus_residual);
revisions.displacement.value++;
mfem::Vector minus_residual;
gravity_operator.Prepare(minus_state, revisions);
gravity_operator.Mult(minus_state, minus_residual);
REQUIRE(plus_residual.Size() == layout.residual_offsets().Last());
REQUIRE(minus_residual.Size() == layout.residual_offsets().Last());
mfem::Vector finite_difference(plus_residual);
finite_difference -= minus_residual;
finite_difference /= 2.0 * difference_step;
return finite_difference;
};
constexpr std::array<double, 3> difference_steps{8.0e-2, 4.0e-2, 2.0e-2};
std::array<double, difference_steps.size()> errors{};
std::array<double, difference_steps.size()> finite_difference_norms{};
for (std::size_t i = 0; i < difference_steps.size(); ++i) {
const mfem::Vector finite_difference = evaluate_geometry_centered_difference(difference_steps[i]);
finite_difference_norms[i] = global_vector_norm(finite_difference, communicator);
errors[i] = global_relative_error(finite_difference, jacobian_action, communicator);
INFO(
"Step = " << difference_steps[i] << ", finite-difference norm = " << finite_difference_norms[i]
<< ", relative error = " << errors[i]
);
REQUIRE(finite_difference_norms[i] > 1.0e-12);
REQUIRE(std::isfinite(errors[i]));
REQUIRE(errors[i] > 0.0);
}
const double first_observed_order = std::log(errors[0] / errors[1]) / std::log(2.0);
const double second_observed_order = std::log(errors[1] / errors[2]) / std::log(2.0);
INFO(
"Difference steps = [" << difference_steps[0] << ", " << difference_steps[1] << ", " << difference_steps[2]
<< "]"
);
INFO(
"Finite-difference norms = [" << finite_difference_norms[0] << ", " << finite_difference_norms[1] << ", "
<< finite_difference_norms[2] << "]"
);
INFO("Relative errors = [" << errors[0] << ", " << errors[1] << ", " << errors[2] << "]");
INFO("First observed convergence order = " << first_observed_order);
INFO("Second observed convergence order = " << second_observed_order);
REQUIRE(std::isfinite(first_observed_order));
REQUIRE(std::isfinite(second_observed_order));
CHECK(errors[1] < errors[0]);
CHECK(errors[2] < errors[1]);
CHECK(first_observed_order > 1.8);
CHECK(second_observed_order > 1.8);
}
TEST_CASE(
"Gravity Field Jacobian Complete Coupled Direction Matches Centered "
"Differences",
tags::integration &tags::solver &tags::gravity &tags::mfem_operators
) {
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);
REQUIRE(f.domainMapperStateless != nullptr);
const gravity_layout layout = make_gravity_jacobian_layout(f);
const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f);
const mfem::Vector state = make_gravity_jacobian_state(f, layout, true);
mfem::Vector direction = make_combined_fixed_geometry_direction(layout);
set_value_block(direction, layout, displacement_block, fields.displacement_direction_2);
operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(
f, *f.domainMapperStateless
);
operators::GravityFieldJacobianOperator gravity_jacobian(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()
);
operators::GravityFieldOperator gravity_operator(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian
);
operators::context::gravity_field::GravityFieldRevisions base_revisions{
.discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1}
};
MPI_Comm communicator = f.mesh->GetComm();
REQUIRE(state.Size() == layout.value_offsets().Last());
REQUIRE(direction.Size() == layout.value_offsets().Last());
REQUIRE(global_vector_norm(direction, communicator) > 1.0e-12);
gravity_operator.Prepare(state, base_revisions);
REQUIRE(linearization_context.IsPrepared());
check_linearization_context_matches_state(linearization_context, state, layout.value_offsets(), communicator);
mfem::Operator &gradient = gravity_operator.GetGradient(state);
REQUIRE(&gradient == static_cast<mfem::Operator *>(&gravity_jacobian));
mfem::Vector jacobian_action;
gradient.Mult(direction, jacobian_action);
REQUIRE(jacobian_action.Size() == layout.residual_offsets().Last());
constexpr double difference_step = 1.0e-5;
mfem::Vector plus_state(state);
mfem::Vector minus_state(state);
plus_state.Add(difference_step, direction);
minus_state.Add(-difference_step, direction);
auto plus_revisions = base_revisions;
plus_revisions.displacement.value++;
plus_revisions.density.value++;
plus_revisions.gravity_gradient.value++;
plus_revisions.gravity_potential.value++;
mfem::Vector plus_residual;
gravity_operator.Prepare(plus_state, plus_revisions);
gravity_operator.Mult(plus_state, plus_residual);
REQUIRE(plus_residual.Size() == layout.residual_offsets().Last());
auto minus_revisions = plus_revisions;
minus_revisions.displacement.value++;
minus_revisions.density.value++;
minus_revisions.gravity_gradient.value++;
minus_revisions.gravity_potential.value++;
mfem::Vector minus_residual;
gravity_operator.Prepare(minus_state, minus_revisions);
gravity_operator.Mult(minus_state, minus_residual);
REQUIRE(minus_residual.Size() == layout.residual_offsets().Last());
mfem::Vector finite_difference(plus_residual);
finite_difference -= minus_residual;
finite_difference /= 2.0 * difference_step;
const mfem::Vector gradient_action = get_residual_block(jacobian_action, layout, gravity_gradient_residual_block);
const mfem::Vector poisson_action = get_residual_block(jacobian_action, layout, gravity_poisson_residual_block);
const mfem::Vector finite_difference_gradient =
get_residual_block(finite_difference, layout, gravity_gradient_residual_block);
const mfem::Vector finite_difference_poisson =
get_residual_block(finite_difference, layout, gravity_poisson_residual_block);
const double jacobian_gradient_norm = global_vector_norm(gradient_action, communicator);
const double jacobian_poisson_norm = global_vector_norm(poisson_action, communicator);
const double finite_difference_gradient_norm = global_vector_norm(finite_difference_gradient, communicator);
const double finite_difference_poisson_norm = global_vector_norm(finite_difference_poisson, communicator);
constexpr double nonzero_tolerance = 1.0e-12;
REQUIRE(jacobian_gradient_norm > nonzero_tolerance);
REQUIRE(jacobian_poisson_norm > nonzero_tolerance);
REQUIRE(finite_difference_gradient_norm > nonzero_tolerance);
REQUIRE(finite_difference_poisson_norm > nonzero_tolerance);
mfem::Vector gradient_difference(gradient_action);
gradient_difference -= finite_difference_gradient;
mfem::Vector poisson_difference(poisson_action);
poisson_difference -= finite_difference_poisson;
mfem::Vector combined_difference(jacobian_action);
combined_difference -= finite_difference;
const double absolute_gradient_error = global_vector_norm(gradient_difference, communicator);
const double absolute_poisson_error = global_vector_norm(poisson_difference, communicator);
const double absolute_combined_error = global_vector_norm(combined_difference, communicator);
const double gradient_error = global_relative_error(gradient_action, finite_difference_gradient, communicator);
const double poisson_error = global_relative_error(poisson_action, finite_difference_poisson, communicator);
const double combined_error = global_relative_error(jacobian_action, finite_difference, communicator);
INFO("Jacobian gradient action norm = " << jacobian_gradient_norm);
INFO("Finite-difference gradient norm = " << finite_difference_gradient_norm);
INFO("Jacobian Poisson action norm = " << jacobian_poisson_norm);
INFO("Finite-difference Poisson norm = " << finite_difference_poisson_norm);
INFO("Absolute gradient residual error = " << absolute_gradient_error);
INFO("Absolute Poisson residual error = " << absolute_poisson_error);
INFO("Absolute combined residual error = " << absolute_combined_error);
INFO("Gradient residual relative error = " << gradient_error);
INFO("Poisson residual relative error = " << poisson_error);
INFO("Combined residual relative error = " << combined_error);
REQUIRE(std::isfinite(gradient_error));
REQUIRE(std::isfinite(poisson_error));
REQUIRE(std::isfinite(combined_error));
CHECK_THAT(gradient_error, WithinAbs(0.0, 5.0e-7));
CHECK_THAT(poisson_error, WithinAbs(0.0, 5.0e-7));
CHECK_THAT(combined_error, WithinAbs(0.0, 5.0e-7));
}
TEST_CASE(
"Reduced Gravity Field Operator Solves Deformed Gravity System With MINRES",
tags::integration &tags::solver &tags::gravity &tags::mfem_operators
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.mapping != nullptr);
REQUIRE(f.domainMapperStateless != nullptr);
const gravity_layout layout = make_gravity_jacobian_layout(f);
const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f);
const mfem::Vector density = make_source_variation_density(f);
const mfem::Vector displacement = fields.displacement;
mfem::ParGridFunction legacy_displacement(f.displacementFes.get());
legacy_displacement.SetFromTrueDofs(displacement);
f.mapping->SetDisplacement(legacy_displacement);
physics::update_stiffness_matrix(f);
REQUIRE(f.gravityContext.b_form != nullptr);
REQUIRE(f.gravityContext.BT != nullptr);
REQUIRE(f.gravityContext.block_prec != nullptr);
operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(
f, *f.domainMapperStateless
);
operators::GravityFieldJacobianOperator gravity_jacobian(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()
);
operators::GravityFieldOperator gravity_operator(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian
);
operators::context::gravity_field::GravityFieldGeometryContext reduced_geometry_context(
f, *f.domainMapperStateless
);
operators::ReducedGravityFieldOperator reduced_operator(gravity_operator, reduced_geometry_context, displacement);
REQUIRE(reduced_geometry_context.IsPrepared());
REQUIRE(reduced_operator.Width() == layout.residual_offsets().Last());
REQUIRE(reduced_operator.Height() == layout.residual_offsets().Last());
REQUIRE(reduced_operator.Width() == reduced_operator.Height());
const std::uint64_t mass_preparations_before_solve =
reduced_geometry_context.GetMassOperator().GetPreparationCount();
const std::uint64_t source_preparations_before_solve =
reduced_geometry_context.GetSourceOperator().GetPreparationCount();
REQUIRE(mass_preparations_before_solve > 0);
REQUIRE(source_preparations_before_solve > 0);
mfem::Vector right_hand_side;
reduced_operator.BuildRightHandSide(density, right_hand_side);
REQUIRE(right_hand_side.Size() == reduced_operator.Height());
MPI_Comm communicator = f.mesh->GetComm();
const double right_hand_side_norm = global_norm(right_hand_side, communicator);
INFO("Reduced gravity-system size = " << reduced_operator.Height());
INFO("Gravity right-hand-side norm = " << right_hand_side_norm);
INFO("Mass preparations before solve = " << mass_preparations_before_solve);
INFO("Source preparations before solve = " << source_preparations_before_solve);
REQUIRE(right_hand_side_norm > 0.0);
mfem::Vector gravity_state(reduced_operator.Width());
gravity_state = 0.0;
constexpr double relative_solver_tolerance = 1.0e-14;
constexpr double absolute_solver_tolerance = 1.0e-14;
const int maximum_iterations = 2000;
mfem::MINRESSolver minres(communicator);
minres.SetOperator(reduced_operator);
minres.SetPreconditioner(*f.gravityContext.block_prec);
minres.SetRelTol(relative_solver_tolerance);
minres.SetAbsTol(absolute_solver_tolerance);
minres.SetMaxIter(maximum_iterations);
minres.SetPrintLevel(1);
MEAN_FIELD_PROFILE_RESET();
MEAN_FIELD_PROFILE_CALL_WARMUP("MINRES total", 0, minres.Mult(right_hand_side, gravity_state));
MEAN_FIELD_PROFILE_PRINT(communicator);
REQUIRE(gravity_state.Size() == reduced_operator.Width());
const std::uint64_t mass_preparations_after_solve =
reduced_geometry_context.GetMassOperator().GetPreparationCount();
const std::uint64_t source_preparations_after_solve =
reduced_geometry_context.GetSourceOperator().GetPreparationCount();
INFO("Mass preparations after solve = " << mass_preparations_after_solve);
INFO("Source preparations after solve = " << source_preparations_after_solve);
CHECK(mass_preparations_after_solve == mass_preparations_before_solve);
CHECK(source_preparations_after_solve == source_preparations_before_solve);
mfem::Vector operator_action;
reduced_operator.Mult(gravity_state, operator_action);
mfem::Vector reduced_residual(operator_action);
reduced_residual -= right_hand_side;
const mfem::Vector gradient_residual =
get_residual_block(reduced_residual, layout, gravity_gradient_residual_block);
const mfem::Vector poisson_residual = get_residual_block(reduced_residual, layout, gravity_poisson_residual_block);
const mfem::Vector solved_gravity_gradient =
get_residual_block(gravity_state, layout, gravity_gradient_residual_block);
const mfem::Vector solved_gravity_potential =
get_residual_block(gravity_state, layout, gravity_poisson_residual_block);
const double gravity_state_norm = global_norm(gravity_state, communicator);
const double gravity_gradient_norm = global_norm(solved_gravity_gradient, communicator);
const double gravity_potential_norm = global_norm(solved_gravity_potential, communicator);
const double residual_norm = global_norm(reduced_residual, communicator);
const double gradient_residual_norm = global_norm(gradient_residual, communicator);
const double poisson_residual_norm = global_norm(poisson_residual, communicator);
const double relative_residual = residual_norm / right_hand_side_norm;
const double relative_gradient_residual = gradient_residual_norm / right_hand_side_norm;
const double relative_poisson_residual = poisson_residual_norm / right_hand_side_norm;
INFO("MINRES converged = " << minres.GetConverged());
INFO("MINRES iterations = " << minres.GetNumIterations());
INFO("MINRES reported final norm = " << minres.GetFinalNorm());
INFO("Gravity-state norm = " << gravity_state_norm);
INFO("Solved gravity-gradient norm = " << gravity_gradient_norm);
INFO("Solved gravity-potential norm = " << gravity_potential_norm);
INFO("Direct residual norm = " << residual_norm);
INFO("Direct relative residual = " << relative_residual);
INFO("Relative gradient-equation residual = " << relative_gradient_residual);
INFO("Relative Poisson-equation residual = " << relative_poisson_residual);
CHECK(minres.GetConverged());
CHECK(minres.GetNumIterations() <= maximum_iterations);
REQUIRE(gravity_state_norm > 0.0);
REQUIRE(gravity_gradient_norm > 0.0);
REQUIRE(gravity_potential_norm > 0.0);
constexpr double direct_residual_tolerance = 1.0e-11;
CHECK_THAT(relative_residual, WithinAbs(0.0, direct_residual_tolerance));
CHECK_THAT(relative_gradient_residual, WithinAbs(0.0, direct_residual_tolerance));
CHECK_THAT(relative_poisson_residual, WithinAbs(0.0, direct_residual_tolerance));
mfem::Vector full_state(layout.value_offsets().Last());
full_state = 0.0;
set_value_block(full_state, layout, density_block, density);
set_value_block(full_state, layout, displacement_block, displacement);
set_value_block(full_state, layout, gravity_gradient_block, solved_gravity_gradient);
set_value_block(full_state, layout, gravity_potential_block, solved_gravity_potential);
const operators::context::gravity_field::GravityFieldRevisions full_state_revisions{
.discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1}
};
gravity_operator.Prepare(full_state, full_state_revisions);
REQUIRE(linearization_context.IsPrepared());
check_linearization_context_matches_state(linearization_context, full_state, layout.value_offsets(), communicator);
mfem::Vector full_residual;
gravity_operator.Mult(full_state, full_residual);
REQUIRE(full_residual.Size() == layout.residual_offsets().Last());
mfem::Vector residual_representation_difference(full_residual);
residual_representation_difference -= reduced_residual;
const double residual_representation_error =
global_norm(residual_representation_difference, communicator) / right_hand_side_norm;
const double full_relative_residual = global_norm(full_residual, communicator) / right_hand_side_norm;
INFO("Full gravity residual relative norm = " << full_relative_residual);
INFO("Reduced/full residual representation error = " << residual_representation_error);
CHECK_THAT(full_relative_residual, WithinAbs(0.0, direct_residual_tolerance));
CHECK_THAT(residual_representation_error, WithinAbs(0.0, 1.0e-12));
}
TEST_CASE(
"Gravity Hdiv Operator Difference Is Localized To Vacuum Compactification",
tags::integration &tags::solver &tags::gravity &tags::mfem_operators &tags::legacy_comparison
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.mapping != nullptr);
REQUIRE(f.domainMapperStateless != nullptr);
REQUIRE(f.compactificationCoordinate != nullptr);
using form = blocks::gravity_field_form;
constexpr auto displacement_block =
mean_field::utils::blocks::get_value_block<form>(blocks::displacement_field.geometry_term);
constexpr auto gravity_gradient_block =
mean_field::utils::blocks::get_value_block<form>(blocks::gravity_field.gradient_term);
constexpr auto gravity_gradient_residual_block =
mean_field::utils::blocks::get_residual_block<form>(blocks::gravity_field.gradient_term);
const std::array<int, form::value_block_count> value_sizes{
f.densityFes->GetTrueVSize(), f.displacementFes->GetTrueVSize(), f.gravityFluxFes->GetTrueVSize(),
f.gravityPotentialFes->GetTrueVSize()
};
const std::array<int, form::residual_block_count> residual_sizes{
f.gravityFluxFes->GetTrueVSize(), f.gravityPotentialFes->GetTrueVSize()
};
const blocks::form_layout<form> layout(value_sizes, residual_sizes);
const mfem::Vector gravity_gradient_true = make_full_support_gravity_gradient(f);
MPI_Comm communicator = f.gravityFluxFes->GetComm();
REQUIRE(global_vector_norm(gravity_gradient_true, communicator) > 0.0);
for (const bool deformed : std::array{false, true}) {
DYNAMIC_SECTION("Geometry = " << (deformed ? "deformed" : "identity")) {
const mfem::Vector displacement_true = make_stateless_reference_displacement(f, deformed);
/*
* Prepare the legacy geometry and assemble its PA operator.
* The legacy mapper performs its vacuum map without consulting
* the exterior-coordinate GridFunction.
*/
mfem::ParGridFunction legacy_displacement(f.displacementFes.get());
legacy_displacement.SetFromTrueDofs(displacement_true);
f.mapping->SetDisplacement(legacy_displacement);
physics::update_stiffness_matrix(f);
REQUIRE(f.gravityContext.m_form != nullptr);
operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(
f, *f.domainMapperStateless
);
operators::GravityFieldJacobianOperator gravity_jacobian(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()
);
operators::GravityFieldOperator gravity_operator(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian
);
mfem::Vector state(layout.value_offsets().Last());
state = 0.0;
for (int i = 0; i < displacement_true.Size(); ++i) {
state(layout.offset(displacement_block) + i) = displacement_true(i);
}
for (int i = 0; i < gravity_gradient_true.Size(); ++i) {
state(layout.offset(gravity_gradient_block) + i) = gravity_gradient_true(i);
}
const operators::context::gravity_field::GravityFieldRevisions revisions{
.discretization = {1},
.displacement = {1},
.density = {1},
.gravity_gradient = {1},
.gravity_potential = {1}
};
gravity_operator.Prepare(state, revisions);
mfem::Vector new_residual;
gravity_operator.Mult(state, new_residual);
mfem::Vector new_operator_mass_action(layout.size(gravity_gradient_residual_block));
for (int i = 0; i < new_operator_mass_action.Size(); ++i) {
new_operator_mass_action(i) = new_residual(layout.offset(gravity_gradient_residual_block) + i);
}
mfem::Vector legacy_operator_mass_action(f.gravityFluxFes->GetTrueVSize());
legacy_operator_mass_action = 0.0;
f.gravityContext.m_form->Mult(gravity_gradient_true, legacy_operator_mass_action);
const StatelessHDivMassReference new_reference =
evaluate_stateless_hdiv_mass_quadrature_reference(f, gravity_gradient_true, displacement_true);
const StatelessHDivMassReference legacy_reference =
assemble_legacy_hdiv_mass_reference(f, gravity_gradient_true);
mfem::Vector new_decomposed_action(new_reference.stellar_action);
new_decomposed_action += new_reference.vacuum_action;
mfem::Vector legacy_decomposed_action(legacy_reference.stellar_action);
legacy_decomposed_action += legacy_reference.vacuum_action;
mfem::Vector total_gap(new_reference.total_action);
total_gap -= legacy_reference.total_action;
mfem::Vector stellar_gap(new_reference.stellar_action);
stellar_gap -= legacy_reference.stellar_action;
mfem::Vector vacuum_gap(new_reference.vacuum_action);
vacuum_gap -= legacy_reference.vacuum_action;
mfem::Vector decomposed_gap(stellar_gap);
decomposed_gap += vacuum_gap;
mfem::Vector unexplained_by_vacuum(total_gap);
unexplained_by_vacuum -= vacuum_gap;
const double new_operator_reference_error =
global_relative_difference(new_operator_mass_action, new_reference.total_action, communicator);
const double legacy_operator_reference_error =
global_relative_difference(legacy_operator_mass_action, legacy_reference.total_action, communicator);
const double new_decomposition_error =
global_relative_difference(new_reference.total_action, new_decomposed_action, communicator);
const double legacy_decomposition_error =
global_relative_difference(legacy_reference.total_action, legacy_decomposed_action, communicator);
const double gap_decomposition_error = global_relative_difference(total_gap, decomposed_gap, communicator);
const double stellar_relative_difference =
global_relative_difference(new_reference.stellar_action, legacy_reference.stellar_action, communicator);
const double vacuum_relative_difference =
global_relative_difference(new_reference.vacuum_action, legacy_reference.vacuum_action, communicator);
const double total_relative_difference =
global_relative_difference(new_reference.total_action, legacy_reference.total_action, communicator);
const double total_gap_norm = global_vector_norm(total_gap, communicator);
const double stellar_gap_norm = global_vector_norm(stellar_gap, communicator);
const double vacuum_gap_norm = global_vector_norm(vacuum_gap, communicator);
const double unexplained_gap_norm = global_vector_norm(unexplained_by_vacuum, communicator);
const double stellar_fraction_of_gap =
stellar_gap_norm / std::max(total_gap_norm, std::numeric_limits<double>::epsilon());
const double unexplained_fraction_of_gap =
unexplained_gap_norm / std::max(total_gap_norm, std::numeric_limits<double>::epsilon());
const double new_total_energy =
global_vector_dot(gravity_gradient_true, new_reference.total_action, communicator);
const double legacy_total_energy =
global_vector_dot(gravity_gradient_true, legacy_reference.total_action, communicator);
const double new_stellar_energy =
global_vector_dot(gravity_gradient_true, new_reference.stellar_action, communicator);
const double legacy_stellar_energy =
global_vector_dot(gravity_gradient_true, legacy_reference.stellar_action, communicator);
const double new_vacuum_energy =
global_vector_dot(gravity_gradient_true, new_reference.vacuum_action, communicator);
const double legacy_vacuum_energy =
global_vector_dot(gravity_gradient_true, legacy_reference.vacuum_action, communicator);
const double total_energy_gap = new_total_energy - legacy_total_energy;
const double stellar_energy_gap = new_stellar_energy - legacy_stellar_energy;
const double vacuum_energy_gap = new_vacuum_energy - legacy_vacuum_energy;
const double energy_gap_decomposition_error =
std::abs(total_energy_gap - stellar_energy_gap - vacuum_energy_gap) /
std::max(std::abs(total_energy_gap), std::numeric_limits<double>::epsilon());
INFO("Geometry = " << (deformed ? "deformed" : "identity"));
INFO("New operator/reference error = " << new_operator_reference_error);
INFO("Legacy operator/reference error = " << legacy_operator_reference_error);
INFO("New regional decomposition error = " << new_decomposition_error);
INFO("Legacy regional decomposition error = " << legacy_decomposition_error);
INFO("Gap decomposition error = " << gap_decomposition_error);
INFO("New/legacy total mass-action difference = " << total_relative_difference);
INFO("New/legacy stellar mass-action difference = " << stellar_relative_difference);
INFO("New/legacy vacuum mass-action difference = " << vacuum_relative_difference);
INFO("Total operator-gap norm = " << total_gap_norm);
INFO("Stellar operator-gap norm = " << stellar_gap_norm);
INFO("Vacuum operator-gap norm = " << vacuum_gap_norm);
INFO("Stellar fraction of operator gap = " << stellar_fraction_of_gap);
INFO("Operator gap unexplained by vacuum = " << unexplained_fraction_of_gap);
INFO("New total H(div) energy = " << new_total_energy);
INFO("Legacy total H(div) energy = " << legacy_total_energy);
INFO("New stellar H(div) energy = " << new_stellar_energy);
INFO("Legacy stellar H(div) energy = " << legacy_stellar_energy);
INFO("New vacuum H(div) energy = " << new_vacuum_energy);
INFO("Legacy vacuum H(div) energy = " << legacy_vacuum_energy);
INFO("Total H(div) energy gap = " << total_energy_gap);
INFO("Stellar H(div) energy gap = " << stellar_energy_gap);
INFO("Vacuum H(div) energy gap = " << vacuum_energy_gap);
INFO("Energy-gap decomposition error = " << energy_gap_decomposition_error);
REQUIRE(new_reference.stellar_elements > 0);
REQUIRE(new_reference.vacuum_elements > 0);
REQUIRE(legacy_reference.stellar_elements > 0);
REQUIRE(legacy_reference.vacuum_elements > 0);
REQUIRE(new_total_energy > 0.0);
REQUIRE(legacy_total_energy > 0.0);
REQUIRE(new_stellar_energy > 0.0);
REQUIRE(legacy_stellar_energy > 0.0);
REQUIRE(new_vacuum_energy > 0.0);
REQUIRE(legacy_vacuum_energy > 0.0);
constexpr double operator_reference_tolerance = 1.0e-11;
constexpr double decomposition_tolerance = 5.0e-13;
constexpr double stellar_parity_tolerance = 1.0e-10;
constexpr double gap_localization_tolerance = 1.0e-9;
CHECK_THAT(new_operator_reference_error, Catch::Matchers::WithinAbs(0.0, operator_reference_tolerance));
CHECK_THAT(legacy_operator_reference_error, Catch::Matchers::WithinAbs(0.0, operator_reference_tolerance));
CHECK_THAT(new_decomposition_error, Catch::Matchers::WithinAbs(0.0, decomposition_tolerance));
CHECK_THAT(legacy_decomposition_error, Catch::Matchers::WithinAbs(0.0, decomposition_tolerance));
CHECK_THAT(gap_decomposition_error, Catch::Matchers::WithinAbs(0.0, decomposition_tolerance));
CHECK_THAT(stellar_relative_difference, Catch::Matchers::WithinAbs(0.0, stellar_parity_tolerance));
/*
* The previous global comparison already showed a material
* difference. Confirm that this test continues to exercise it.
*/
REQUIRE(total_gap_norm > 1.0e-12);
REQUIRE(vacuum_gap_norm > 1.0e-12);
REQUIRE(vacuum_relative_difference > 1.0e-8);
/*
* If these pass, the old/new difference has been directly
* localized to the vacuum compactification prescription.
*/
CHECK_THAT(stellar_fraction_of_gap, Catch::Matchers::WithinAbs(0.0, gap_localization_tolerance));
CHECK_THAT(unexplained_fraction_of_gap, Catch::Matchers::WithinAbs(0.0, gap_localization_tolerance));
CHECK_THAT(energy_gap_decomposition_error, Catch::Matchers::WithinAbs(0.0, decomposition_tolerance));
}
}
}