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

722 lines
25 KiB
C++

#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <memory>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
import mean_field;
import test_helpers;
namespace prepared_pressure_force_test_utils {
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
using CoupledForm = mean_field::utils::blocks::barotropic_equilibrium_form;
struct Maps final {
mean_field::field::FieldDofMap density;
mean_field::field::FieldDofMap displacement;
mean_field::field::FieldDofMap gravityFlux;
mean_field::field::FieldDofMap gravityPotential;
mean_field::field::FieldDofMap enthalpy;
explicit Maps(const mean_field::fem::FEM &f)
: density(
mean_field::field::make_field_dof_map<mean_field::field::Density,
DomainSchema>(*f.densityFes)),
displacement(mean_field::field::make_field_dof_map<
mean_field::field::Displacement, DomainSchema>(
*f.displacementFes)),
gravityFlux(
mean_field::field::make_field_dof_map<mean_field::field::Gravity,
DomainSchema>(
*f.gravityFluxFes)),
gravityPotential(
mean_field::field::make_field_dof_map<mean_field::field::Gravity,
DomainSchema>(
*f.gravityPotentialFes)),
enthalpy(mean_field::field::make_field_dof_map<
mean_field::field::Enthalpy, DomainSchema>(*f.enthalpyFes)) {}
};
[[nodiscard]]
mfem::Vector make_positive_enthalpy_true(const mean_field::fem::FEM &f,
const double phase) {
mfem::Vector enthalpy(f.enthalpyFes->GetTrueVSize());
for (int index = 0; index < enthalpy.Size(); ++index) {
const double position = static_cast<double>(index + 1);
enthalpy(index) = 0.93 + 0.09 * std::sin(0.23 * position + phase) +
0.04 * std::cos(0.17 * position - 0.5 * phase);
}
return enthalpy;
}
[[nodiscard]]
mfem::Vector make_enthalpy_direction_true(const mean_field::fem::FEM &f,
const double phase) {
mfem::Vector direction(f.enthalpyFes->GetTrueVSize());
for (int index = 0; index < direction.Size(); ++index) {
const double position = static_cast<double>(index + 1);
direction(index) = 0.27 * std::sin(0.19 * position + phase) +
0.14 * std::cos(0.13 * position - 0.5 * phase);
}
return direction;
}
[[nodiscard]]
mfem::Vector make_displacement_direction_true(const mean_field::fem::FEM &f,
const double phase) {
MFEM_VERIFY(f.mesh->Dimension() == 3,
"The prepared pressure-force test requires a "
"three-dimensional mesh.");
mfem::ParGridFunction directionField(f.displacementFes.get());
mfem::VectorFunctionCoefficient directionCoefficient(
3, [phase](const mfem::Vector &position, mfem::Vector &value) {
const double x = position(0);
const double y = position(1);
const double z = position(2);
value.SetSize(3);
value(0) =
0.019 * x + 0.011 * y * z - 0.006 * z * z + 0.004 * phase * y;
value(1) =
-0.016 * y + 0.008 * x * z + 0.005 * x * x - 0.003 * phase * z;
value(2) =
0.013 * z - 0.010 * x * y + 0.006 * y * y + 0.004 * phase * x;
});
directionField.ProjectCoefficient(directionCoefficient);
mfem::Vector directionTrue;
directionField.GetTrueDofs(directionTrue);
return directionTrue;
}
[[nodiscard]]
double relative_difference(const mfem::Vector &left, const mfem::Vector &right,
const MPI_Comm communicator) {
MFEM_VERIFY(left.Size() == right.Size(),
"Cannot compare prepared pressure-force vectors with "
"different sizes.");
mfem::Vector difference(left);
difference -= right;
const double scale =
std::max({gravity_prepared_test_utils::global_norm(left, communicator),
gravity_prepared_test_utils::global_norm(right, communicator),
100.0 * std::numeric_limits<double>::epsilon()});
return gravity_prepared_test_utils::global_norm(difference, communicator) /
scale;
}
[[nodiscard]]
mean_field::operators::context::pressure_force::PressureForceDependencies
make_dependencies() {
return {.discretization = {.identity = 1201, .revision = 3},
.enthalpy = {.identity = 1213, .revision = 5},
.displacement = {.identity = 1217, .revision = 7}};
}
constexpr auto densityValue =
mean_field::utils::blocks::get_value_block<CoupledForm>(
mean_field::utils::blocks::density_field.mass_term);
constexpr auto displacementValue =
mean_field::utils::blocks::get_value_block<CoupledForm>(
mean_field::utils::blocks::displacement_field.geometry_term);
constexpr auto gravityGradientValue =
mean_field::utils::blocks::get_value_block<CoupledForm>(
mean_field::utils::blocks::gravity_field.gradient_term);
constexpr auto gravityPotentialValue =
mean_field::utils::blocks::get_value_block<CoupledForm>(
mean_field::utils::blocks::gravity_field.poisson_term);
constexpr auto enthalpyValue =
mean_field::utils::blocks::get_value_block<CoupledForm>(
mean_field::utils::blocks::enthalpy_field.specific_term);
constexpr auto barotropicConstantValue =
mean_field::utils::blocks::get_value_block<CoupledForm>(
mean_field::utils::blocks::barotropic_constant_field
.mass_normalization_term);
constexpr auto gravityGradientResidual =
mean_field::utils::blocks::get_residual_block<CoupledForm>(
mean_field::utils::blocks::gravity_field.gradient_term);
constexpr auto gravityPotentialResidual =
mean_field::utils::blocks::get_residual_block<CoupledForm>(
mean_field::utils::blocks::gravity_field.poisson_term);
constexpr auto densityResidual =
mean_field::utils::blocks::get_residual_block<CoupledForm>(
mean_field::utils::blocks::density_field.mass_term);
constexpr auto displacementResidual =
mean_field::utils::blocks::get_residual_block<CoupledForm>(
mean_field::utils::blocks::displacement_field.geometry_term);
constexpr auto enthalpyResidual =
mean_field::utils::blocks::get_residual_block<CoupledForm>(
mean_field::utils::blocks::enthalpy_field.specific_term);
constexpr auto massResidual =
mean_field::utils::blocks::get_residual_block<CoupledForm>(
mean_field::utils::blocks::barotropic_constant_field
.mass_normalization_term);
[[nodiscard]]
mean_field::operators::BarotropicEquilibriumLayout
make_coupled_layout(const Maps &maps) {
const std::array<int, CoupledForm::value_block_count> valueSizes{
maps.density.reduced_size(), maps.displacement.reduced_size(),
maps.gravityFlux.reduced_size(), maps.gravityPotential.reduced_size(),
maps.enthalpy.reduced_size(), 1};
const std::array<int, CoupledForm::residual_block_count> residualSizes{
maps.gravityFlux.reduced_size(), maps.gravityPotential.reduced_size(),
maps.density.reduced_size(), maps.displacement.reduced_size(),
maps.enthalpy.reduced_size(), 1};
return {valueSizes, residualSizes};
}
template <int index>
[[nodiscard]]
mfem::Vector copy_residual_block(
const mfem::Vector &action,
const mean_field::operators::BarotropicEquilibriumLayout &layout,
const mean_field::utils::blocks::residual_block<index> block) {
mfem::Vector result(layout.size(block));
const int offset = layout.offset(block);
for (int entry = 0; entry < result.Size(); ++entry) {
result(entry) = action(offset + entry);
}
return result;
}
} // namespace prepared_pressure_force_test_utils
TEST_CASE(
"Prepared Pressure Force Uses FieldDof Supported Dimensions And Owns Its "
"Context",
tags::barotrope &tags::pressure &tags::prepared &tags::field &tags::unit) {
mean_field::utils::Args args = test_utils::setup_args();
mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.okay());
const prepared_pressure_force_test_utils::Maps maps(f);
const mean_field::eos::Polytrope equationOfState(3.0, 0.25);
mean_field::operators::PreparedPressureForceOperator preparedOperator(
f, *f.domainMapperStateless, equationOfState);
REQUIRE(maps.enthalpy.reduced_size() < maps.enthalpy.full_size());
CHECK(maps.displacement.is_identity());
CHECK(preparedOperator.GetEnthalpySize() == maps.enthalpy.reduced_size());
CHECK(preparedOperator.GetDisplacementSize() ==
maps.displacement.reduced_size());
CHECK(&preparedOperator.GetContext().GetPreparationStatistics() ==
&preparedOperator.GetContextPreparationStatistics());
}
TEST_CASE("Prepared Pressure Force Jacobian Matches Full Stateless Columns "
"Through FieldDof Restriction",
tags::barotrope &tags::pressure &tags::prepared &tags::field
&tags::integration &tags::accuracy) {
mean_field::utils::Args args = test_utils::setup_args();
mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.okay());
const prepared_pressure_force_test_utils::Maps maps(f);
const mean_field::eos::Polytrope equationOfState(3.0, 0.25);
const mfem::Vector enthalpy = maps.enthalpy.gather(
prepared_pressure_force_test_utils::make_positive_enthalpy_true(f, 0.47));
const mfem::Vector displacement = maps.displacement.gather(
gravity_prepared_test_utils::make_displacement(f, 0.69));
const mfem::Vector enthalpyDirection = maps.enthalpy.gather(
prepared_pressure_force_test_utils::make_enthalpy_direction_true(f,
0.73));
const mfem::Vector displacementDirection = maps.displacement.gather(
prepared_pressure_force_test_utils::make_displacement_direction_true(
f, 0.83));
mean_field::operators::PreparedPressureForceOperator preparedOperator(
f, *f.domainMapperStateless, equationOfState);
preparedOperator.Prepare(
{.enthalpy = enthalpy, .displacement = displacement},
prepared_pressure_force_test_utils::make_dependencies());
const mfem::Vector enthalpyTrue = maps.enthalpy.scatter(enthalpy);
const mfem::Vector displacementTrue = maps.displacement.scatter(displacement);
const mfem::Vector enthalpyDirectionTrue =
maps.enthalpy.scatter(enthalpyDirection);
const mfem::Vector displacementDirectionTrue =
maps.displacement.scatter(displacementDirection);
mfem::Vector preparedEnthalpyAction;
mfem::Vector kernelEnthalpyActionTrue;
preparedOperator.ApplyEnthalpyJacobianAction(enthalpyDirection,
preparedEnthalpyAction);
mean_field::operators::kernels::apply_pressure_force_enthalpy_action(
f, *f.domainMapperStateless, equationOfState, enthalpyTrue,
enthalpyDirectionTrue, displacementTrue, kernelEnthalpyActionTrue);
const mfem::Vector kernelEnthalpyAction =
maps.displacement.gather(kernelEnthalpyActionTrue);
CHECK(prepared_pressure_force_test_utils::relative_difference(
preparedEnthalpyAction, kernelEnthalpyAction, f.mesh->GetComm()) <
2.0e-12);
mfem::Vector preparedDisplacementAction;
mfem::Vector kernelDisplacementActionTrue;
preparedOperator.ApplyDisplacementJacobianAction(displacementDirection,
preparedDisplacementAction);
mean_field::operators::kernels::apply_pressure_force_displacement_action(
f, *f.domainMapperStateless, equationOfState, enthalpyTrue,
displacementDirectionTrue, displacementTrue,
kernelDisplacementActionTrue);
const mfem::Vector kernelDisplacementAction =
maps.displacement.gather(kernelDisplacementActionTrue);
CHECK(prepared_pressure_force_test_utils::relative_difference(
preparedDisplacementAction, kernelDisplacementAction,
f.mesh->GetComm()) < 2.0e-12);
mfem::Vector fusedAction;
preparedOperator.ApplyCompleteJacobianAction(
enthalpyDirection, displacementDirection, fusedAction);
mfem::Vector expectedFusedAction(kernelEnthalpyAction);
expectedFusedAction += kernelDisplacementAction;
CHECK(prepared_pressure_force_test_utils::relative_difference(
fusedAction, expectedFusedAction, f.mesh->GetComm()) < 2.0e-12);
}
TEST_CASE(
"Prepared Pressure Force MFEM Adapter Routes Reduced Coupled FieldDof "
"Blocks",
tags::barotrope &tags::pressure &tags::prepared &tags::field
&tags::integration &tags::jacobian &tags::mfem_operators &tags::unit) {
mean_field::utils::Args args = test_utils::setup_args();
mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.okay());
const prepared_pressure_force_test_utils::Maps maps(f);
const mean_field::eos::Polytrope equationOfState(3.0, 0.25);
const mfem::Vector enthalpy = maps.enthalpy.gather(
prepared_pressure_force_test_utils::make_positive_enthalpy_true(f, 0.53));
const mfem::Vector displacement = maps.displacement.gather(
gravity_prepared_test_utils::make_displacement(f, 0.71));
const mfem::Vector enthalpyDirection = maps.enthalpy.gather(
prepared_pressure_force_test_utils::make_enthalpy_direction_true(f,
0.89));
const mfem::Vector displacementDirection = maps.displacement.gather(
prepared_pressure_force_test_utils::make_displacement_direction_true(
f, 0.97));
mean_field::operators::PreparedPressureForceOperator preparedOperator(
f, *f.domainMapperStateless, equationOfState);
preparedOperator.Prepare(
{.enthalpy = enthalpy, .displacement = displacement},
prepared_pressure_force_test_utils::make_dependencies());
const mean_field::operators::BarotropicEquilibriumLayout layout =
prepared_pressure_force_test_utils::make_coupled_layout(maps);
mean_field::operators::PreparedPressureForceJacobianOperator adapter(
layout, preparedOperator);
CHECK(layout.size(prepared_pressure_force_test_utils::enthalpyValue) ==
maps.enthalpy.reduced_size());
CHECK(layout.size(prepared_pressure_force_test_utils::densityValue) ==
maps.density.reduced_size());
mfem::BlockVector direction(layout.value_offsets());
direction = 0.0;
/*
* Populate unrelated columns deliberately.
*/
direction.GetBlock(prepared_pressure_force_test_utils::densityValue) = 0.37;
direction.GetBlock(prepared_pressure_force_test_utils::gravityGradientValue) =
-0.41;
direction.GetBlock(
prepared_pressure_force_test_utils::gravityPotentialValue) = 0.59;
direction.GetBlock(
prepared_pressure_force_test_utils::barotropicConstantValue) = -0.73;
direction.GetBlock(prepared_pressure_force_test_utils::displacementValue) =
displacementDirection;
direction.GetBlock(prepared_pressure_force_test_utils::enthalpyValue) =
enthalpyDirection;
mfem::Vector expectedDisplacementAction;
preparedOperator.ApplyCompleteJacobianAction(
enthalpyDirection, displacementDirection, expectedDisplacementAction);
mfem::Vector action;
adapter.Mult(direction, action);
const mfem::Vector displacementResidualAction =
prepared_pressure_force_test_utils::copy_residual_block(
action, layout,
prepared_pressure_force_test_utils::displacementResidual);
CHECK(prepared_pressure_force_test_utils::relative_difference(
displacementResidualAction, expectedDisplacementAction,
f.mesh->GetComm()) < 2.0e-14);
CHECK(prepared_pressure_force_test_utils::copy_residual_block(
action, layout,
prepared_pressure_force_test_utils::gravityGradientResidual)
.Norml2() == 0.0);
CHECK(prepared_pressure_force_test_utils::copy_residual_block(
action, layout,
prepared_pressure_force_test_utils::gravityPotentialResidual)
.Norml2() == 0.0);
CHECK(prepared_pressure_force_test_utils::copy_residual_block(
action, layout, prepared_pressure_force_test_utils::densityResidual)
.Norml2() == 0.0);
CHECK(
prepared_pressure_force_test_utils::copy_residual_block(
action, layout, prepared_pressure_force_test_utils::enthalpyResidual)
.Norml2() == 0.0);
CHECK(prepared_pressure_force_test_utils::copy_residual_block(
action, layout, prepared_pressure_force_test_utils::massResidual)
.Norml2() == 0.0);
}
TEST_CASE("Pressure Force Residual Converges To A Manufactured Analytic Force",
tags::barotrope &tags::pressure &tags::kernels &tags::integration
&tags::convergence &tags::h_refinement &tags::analytic_comparison
&tags::accuracy) {
constexpr int dimension = 3;
constexpr std::array<int, 2> refinementLevels{0, 1};
constexpr double minimumObservedRate = 3.0;
constexpr double finestRelativeTolerance = 2.0e-3;
constexpr double amplitude = 1.0;
constexpr double bumpSharpness = 0.25;
constexpr double supportRadiusFraction = 0.90;
std::array<double, refinementLevels.size()> relativeErrors{};
for (std::size_t levelIndex = 0; levelIndex < refinementLevels.size();
++levelIndex) {
mean_field::utils::Args args = test_utils::setup_args();
mean_field::fem::FEM f = mean_field::fem::setup_fem(
args.mesh_file, args, refinementLevels[levelIndex]);
REQUIRE(f.okay());
REQUIRE(f.mesh->Dimension() == dimension);
REQUIRE(f.mesh->GetNE() > 0);
const MPI_Comm communicator = f.mesh->GetComm();
constexpr double supportRadius =
supportRadiusFraction * mean_field::utils::RADIUS;
constexpr double supportRadiusSquared = supportRadius * supportRadius;
auto analyticEnthalpyFunction = [supportRadiusSquared](
const mfem::Vector &position) {
const double normalizedRadiusSquared =
(position * position) / supportRadiusSquared;
if (normalizedRadiusSquared >= 1.0) {
return 0.0;
}
const double distanceToSupportBoundary = 1.0 - normalizedRadiusSquared;
return amplitude * std::exp(-bumpSharpness * normalizedRadiusSquared /
distanceToSupportBoundary);
};
auto analyticPressureForceFunction = [supportRadiusSquared](
const mfem::Vector &position,
mfem::Vector &force) {
force.SetSize(dimension);
force = 0.0;
const double normalizedRadiusSquared =
(position * position) / supportRadiusSquared;
if (normalizedRadiusSquared >= 1.0) {
return;
}
const double distanceToSupportBoundary = 1.0 - normalizedRadiusSquared;
const double enthalpy =
amplitude * std::exp(-bumpSharpness * normalizedRadiusSquared /
distanceToSupportBoundary);
const double pressureGradientScale =
-2.0 * bumpSharpness * std::pow(enthalpy, 4.0) /
(supportRadiusSquared * distanceToSupportBoundary *
distanceToSupportBoundary);
for (int component = 0; component < dimension; ++component) {
force(component) = pressureGradientScale * position(component);
}
};
mfem::FunctionCoefficient analyticEnthalpyCoefficient(
analyticEnthalpyFunction);
mfem::VectorFunctionCoefficient analyticPressureForceCoefficient(
dimension, analyticPressureForceFunction);
mfem::ParGridFunction discreteEnthalpyField(f.enthalpyFes.get());
discreteEnthalpyField.ProjectCoefficient(analyticEnthalpyCoefficient);
mfem::Vector discreteEnthalpyTrue;
discreteEnthalpyField.GetTrueDofs(discreteEnthalpyTrue);
mfem::Vector zeroDisplacement(f.displacementFes->GetTrueVSize());
zeroDisplacement = 0.0;
const mean_field::eos::Polytrope barotrope(3.0, 0.25);
mfem::Vector discreteResidual;
mean_field::operators::kernels::apply_pressure_force_residual(
f, *f.domainMapperStateless, barotrope, discreteEnthalpyTrue,
zeroDisplacement, discreteResidual);
REQUIRE(discreteResidual.Size() == f.displacementFes->GetTrueVSize());
mfem::Array<int> stellarMarker(f.mesh->attributes.Max());
stellarMarker = 0;
const int vacuumAttribute = field_dof_test_utils::vacuum_material_attribute;
for (int attributeIndex = 0; attributeIndex < f.mesh->attributes.Size();
++attributeIndex) {
const int attribute = f.mesh->attributes[attributeIndex];
if (attribute != vacuumAttribute) {
stellarMarker[attribute - 1] = 1;
}
}
const mfem::Geometry::Type elementGeometry =
f.displacementFes->GetFE(0)->GetGeomType();
for (int element = 1; element < f.mesh->GetNE(); ++element) {
REQUIRE(f.displacementFes->GetFE(element)->GetGeomType() ==
elementGeometry);
}
const int referenceQuadratureOrder =
2 * f.displacementFes->GetMaxElementOrder() + 16;
const mfem::IntegrationRule &referenceQuadrature =
mfem::IntRules.Get(elementGeometry, referenceQuadratureOrder);
auto *analyticForceIntegrator =
new mfem::VectorDomainLFIntegrator(analyticPressureForceCoefficient);
analyticForceIntegrator->SetIntRule(&referenceQuadrature);
mfem::ParLinearForm analyticForceLoad(f.displacementFes.get());
analyticForceLoad.AddDomainIntegrator(analyticForceIntegrator,
stellarMarker);
analyticForceLoad.Assemble();
std::unique_ptr<mfem::HypreParVector> analyticForceHypreVector(
analyticForceLoad.ParallelAssemble());
REQUIRE(analyticForceHypreVector != nullptr);
mfem::Vector analyticForceTrue(*analyticForceHypreVector);
REQUIRE(analyticForceTrue.Size() == discreteResidual.Size());
const double analyticForceNorm = gravity_prepared_test_utils::global_norm(
analyticForceTrue, communicator);
REQUIRE(std::isfinite(analyticForceNorm));
REQUIRE(analyticForceNorm > 0.0);
mfem::Vector residualError(discreteResidual);
residualError -= analyticForceTrue;
mfem::ParBilinearForm rieszForm(f.displacementFes.get());
rieszForm.AddDomainIntegrator(new mfem::VectorMassIntegrator());
rieszForm.AddDomainIntegrator(new mfem::VectorDiffusionIntegrator());
rieszForm.Assemble();
rieszForm.Finalize();
std::unique_ptr<mfem::HypreParMatrix> rieszMatrix(
rieszForm.ParallelAssemble());
REQUIRE(rieszMatrix != nullptr);
REQUIRE(rieszMatrix->Height() == discreteResidual.Size());
REQUIRE(rieszMatrix->Width() == discreteResidual.Size());
mfem::HypreBoomerAMG rieszPreconditioner(*rieszMatrix);
rieszPreconditioner.SetPrintLevel(0);
mfem::CGSolver rieszSolver(communicator);
rieszSolver.SetOperator(*rieszMatrix);
rieszSolver.SetPreconditioner(rieszPreconditioner);
rieszSolver.SetRelTol(1.0e-13);
rieszSolver.SetAbsTol(1.0e-15);
rieszSolver.SetMaxIter(5000);
rieszSolver.SetPrintLevel(0);
auto calculateDualNorm = [&rieszSolver,
communicator](const mfem::Vector &functional) {
mfem::Vector rieszRepresentative(functional.Size());
rieszRepresentative = 0.0;
rieszSolver.Mult(functional, rieszRepresentative);
MFEM_VERIFY(rieszSolver.GetConverged(),
"The pressure-force convergence-test Riesz solve "
"did not converge.");
const double dualNormSquared = gravity_prepared_test_utils::global_dot(
functional, rieszRepresentative, communicator);
MFEM_VERIFY(std::isfinite(dualNormSquared),
"The pressure-force dual norm is not finite.");
MFEM_VERIFY(dualNormSquared >=
-100.0 * std::numeric_limits<double>::epsilon(),
"The pressure-force Riesz operator produced a "
"negative dual norm.");
return std::sqrt(std::max(dualNormSquared, 0.0));
};
const double errorDualNorm = calculateDualNorm(residualError);
const double analyticDualNorm = calculateDualNorm(analyticForceTrue);
REQUIRE(std::isfinite(errorDualNorm));
REQUIRE(std::isfinite(analyticDualNorm));
REQUIRE(errorDualNorm > 0.0);
REQUIRE(analyticDualNorm > 0.0);
relativeErrors[levelIndex] = errorDualNorm / analyticDualNorm;
INFO("Pressure-force refinement level = " << refinementLevels[levelIndex]);
INFO("Pressure-force true DOFs = " << f.displacementFes->GlobalTrueVSize());
INFO("Pressure-force relative dual error = " << relativeErrors[levelIndex]);
}
for (const double relativeError : relativeErrors) {
REQUIRE(std::isfinite(relativeError));
REQUIRE(relativeError > 0.0);
}
static_assert(
refinementLevels.size() == 2,
"This reduced convergence test expects exactly two refinement levels.");
const double observedRate =
std::log(relativeErrors[0] / relativeErrors[1]) / std::log(2.0);
INFO("Level 0 pressure-force relative dual error = " << relativeErrors[0]);
INFO("Level 1 pressure-force relative dual error = " << relativeErrors[1]);
INFO("Level 0 to 1 pressure-force convergence rate = " << observedRate);
CHECK(relativeErrors[1] < relativeErrors[0]);
CHECK(observedRate > minimumObservedRate);
CHECK(relativeErrors[1] < finestRelativeTolerance);
}