feat(field-support): added field support system, mid migration

currently the barotope and the pressure force operator are migrated to the new support system
This commit is contained in:
2026-08-23 10:13:53 -04:00
parent dc912fd15e
commit 0f3ca8050b
137 changed files with 29975 additions and 16389 deletions

View File

@@ -0,0 +1,706 @@
#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 = f.domainMapperStateless->GetVacuumElementAttribute();
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(1);
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);
}