674 lines
25 KiB
C++
674 lines
25 KiB
C++
#include <array>
|
|
#include <cmath>
|
|
#include <limits>
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
|
|
#include <mfem.hpp>
|
|
|
|
import mean_field;
|
|
import test_helpers;
|
|
|
|
namespace pressure_force_kernel_test_utils {
|
|
[[nodiscard]] mfem::Vector make_deterministic_vector(
|
|
const int size,
|
|
const double phase
|
|
) {
|
|
mfem::Vector vector(size);
|
|
|
|
for (int index = 0; index < size; ++index) {
|
|
const double position = static_cast<double>(index + 1);
|
|
|
|
vector(index) =
|
|
0.71 + 0.19 * std::sin(0.31 * position + phase) + 0.08 * std::cos(0.17 * position - 0.5 * phase);
|
|
}
|
|
|
|
return vector;
|
|
}
|
|
|
|
[[nodiscard]] mfem::Vector make_zero_displacement(const mean_field::fem::FEM &f) {
|
|
mfem::Vector displacementTrue(f.displacementFes->GetTrueVSize());
|
|
displacementTrue = 0.0;
|
|
return displacementTrue;
|
|
}
|
|
|
|
[[nodiscard]] mfem::Vector make_vacuum_only_enthalpy(const mean_field::fem::FEM &f) {
|
|
mfem::Vector enthalpyTrue = make_deterministic_vector(f.enthalpyFes->GetTrueVSize(), 0.43);
|
|
|
|
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
|
|
const mean_field::field::FieldDofMap enthalpyMap =
|
|
mean_field::field::make_field_dof_map<mean_field::field::Enthalpy, DomainSchema>(*f.enthalpyFes);
|
|
|
|
for (int reducedDof = 0; reducedDof < enthalpyMap.reduced_size(); ++reducedDof) {
|
|
enthalpyTrue(enthalpyMap.true_dof(reducedDof)) = 0.0;
|
|
}
|
|
|
|
return enthalpyTrue;
|
|
}
|
|
|
|
[[nodiscard]] mfem::Vector make_positive_asymmetric_enthalpy(const mean_field::fem::FEM &f) {
|
|
mfem::FunctionCoefficient coefficient([](const mfem::Vector &position) {
|
|
return 1.10 + 0.07 * position(0) - 0.04 * position(1) + 0.03 * position(2);
|
|
});
|
|
|
|
mfem::ParGridFunction enthalpyField(f.enthalpyFes.get());
|
|
|
|
enthalpyField.ProjectCoefficient(coefficient);
|
|
|
|
mfem::Vector enthalpyTrue;
|
|
enthalpyField.GetTrueDofs(enthalpyTrue);
|
|
|
|
return enthalpyTrue;
|
|
}
|
|
|
|
[[nodiscard]] mfem::Vector make_component_test_field(
|
|
const mean_field::fem::FEM &f,
|
|
const int component,
|
|
const int coordinate
|
|
) {
|
|
const int dimension = f.mesh->Dimension();
|
|
|
|
MFEM_VERIFY(component >= 0 && component < dimension, "The requested vector component is invalid.");
|
|
|
|
MFEM_VERIFY(coordinate >= -1 && coordinate < dimension, "The requested coordinate is invalid.");
|
|
|
|
/*
|
|
* coordinate == -1 gives the rigid translation e_component.
|
|
*
|
|
* Otherwise this gives
|
|
*
|
|
* w = x_coordinate e_component.
|
|
*/
|
|
mfem::VectorFunctionCoefficient coefficient(
|
|
dimension, [component, coordinate, dimension](const mfem::Vector &position, mfem::Vector &value) {
|
|
value.SetSize(dimension);
|
|
value = 0.0;
|
|
|
|
value(component) = coordinate < 0 ? 1.0 : position(coordinate);
|
|
}
|
|
);
|
|
|
|
mfem::ParGridFunction field(f.displacementFes.get());
|
|
|
|
field.ProjectCoefficient(coefficient);
|
|
|
|
mfem::Vector fieldTrue;
|
|
field.GetTrueDofs(fieldTrue);
|
|
|
|
return fieldTrue;
|
|
}
|
|
|
|
[[nodiscard]] double global_dot(
|
|
const mfem::Vector &left,
|
|
const mfem::Vector &right,
|
|
MPI_Comm communicator
|
|
) {
|
|
MFEM_VERIFY(left.Size() == right.Size(), "The global dot-product vectors have different sizes.");
|
|
|
|
const double localDot = left * right;
|
|
double globalDot = 0.0;
|
|
|
|
MPI_Allreduce(&localDot, &globalDot, 1, MPI_DOUBLE, MPI_SUM, communicator);
|
|
|
|
return globalDot;
|
|
}
|
|
|
|
[[nodiscard]] double integrate_pressure(
|
|
const mean_field::fem::FEM &f,
|
|
const mean_field::mapping::DomainMapper &domainMapper,
|
|
const mean_field::eos::Polytrope &barotrope,
|
|
const mfem::Vector &enthalpyTrue,
|
|
const mfem::Vector &displacementTrue
|
|
) {
|
|
MFEM_VERIFY(
|
|
enthalpyTrue.Size() == f.enthalpyFes->GetTrueVSize(),
|
|
"The pressure-integral enthalpy vector has the wrong size."
|
|
);
|
|
|
|
MFEM_VERIFY(
|
|
displacementTrue.Size() == f.displacementFes->GetTrueVSize(),
|
|
"The pressure-integral displacement vector has the wrong size."
|
|
);
|
|
|
|
mfem::Vector enthalpyLocal(f.enthalpyFes->GetVSize());
|
|
|
|
const mfem::Operator *enthalpyProlongation = f.enthalpyFes->GetProlongationMatrix();
|
|
|
|
if (enthalpyProlongation != nullptr) {
|
|
enthalpyProlongation->Mult(enthalpyTrue, enthalpyLocal);
|
|
} else {
|
|
enthalpyLocal = enthalpyTrue;
|
|
}
|
|
|
|
mfem::Vector displacementLocal(f.displacementFes->GetVSize());
|
|
|
|
const mfem::Operator *displacementProlongation = f.displacementFes->GetProlongationMatrix();
|
|
|
|
if (displacementProlongation != nullptr) {
|
|
displacementProlongation->Mult(displacementTrue, displacementLocal);
|
|
} else {
|
|
displacementLocal = displacementTrue;
|
|
}
|
|
|
|
const double pressureExtraOrderValue =
|
|
barotrope.polytropic_index() * static_cast<double>(mean_field::field::Enthalpy::Scalar::familyOrder);
|
|
|
|
MFEM_VERIFY(
|
|
std::isfinite(pressureExtraOrderValue) && pressureExtraOrderValue >= 0.0 &&
|
|
pressureExtraOrderValue <= static_cast<double>(std::numeric_limits<int>::max()),
|
|
"The pressure-integral EOS order is invalid."
|
|
);
|
|
|
|
const int pressureExtraOrder = static_cast<int>(std::ceil(pressureExtraOrderValue));
|
|
|
|
using EnthalpyField = mean_field::field::Field<mean_field::field::Enthalpy>;
|
|
|
|
mean_field::mapping::DomainMapper::Workspace workspace(f.mesh->Dimension());
|
|
|
|
mean_field::mapping::VolumeMappingContext mappingContext;
|
|
|
|
mfem::Array<int> enthalpyDofs;
|
|
mfem::Array<int> displacementDofs;
|
|
mfem::Array<int> compactificationDofs;
|
|
|
|
mfem::Vector elementEnthalpy;
|
|
mfem::Vector elementDisplacement;
|
|
mfem::Vector elementCompactification;
|
|
mfem::Vector enthalpyShape;
|
|
|
|
double localPressureIntegral = 0.0;
|
|
|
|
const int vacuumAttribute = field_dof_test_utils::vacuum_material_attribute;
|
|
|
|
for (int elementId = 0; elementId < f.mesh->GetNE(); ++elementId) {
|
|
mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(elementId);
|
|
|
|
MFEM_VERIFY(
|
|
transformation != nullptr, "The pressure-integral reference received a null "
|
|
"element transformation."
|
|
);
|
|
|
|
if (transformation->Attribute == vacuumAttribute) {
|
|
continue;
|
|
}
|
|
|
|
const mfem::FiniteElement &enthalpyElement = *f.enthalpyFes->GetFE(elementId);
|
|
|
|
const mfem::FiniteElement &displacementElement = *f.displacementFes->GetFE(elementId);
|
|
|
|
const mfem::FiniteElement &compactificationElement = *f.compactificationFes->GetFE(elementId);
|
|
|
|
mfem::DofTransformation *enthalpyDofTransformation = f.enthalpyFes->GetElementDofs(elementId, enthalpyDofs);
|
|
|
|
mfem::DofTransformation *displacementDofTransformation =
|
|
f.displacementFes->GetElementVDofs(elementId, displacementDofs);
|
|
|
|
mfem::DofTransformation *compactificationDofTransformation =
|
|
f.compactificationFes->GetElementDofs(elementId, compactificationDofs);
|
|
|
|
enthalpyLocal.GetSubVector(enthalpyDofs, elementEnthalpy);
|
|
|
|
displacementLocal.GetSubVector(displacementDofs, elementDisplacement);
|
|
|
|
f.compactificationCoordinate->GetSubVector(compactificationDofs, elementCompactification);
|
|
|
|
if (enthalpyDofTransformation != nullptr) {
|
|
enthalpyDofTransformation->InvTransformPrimal(elementEnthalpy);
|
|
}
|
|
|
|
if (displacementDofTransformation != nullptr) {
|
|
displacementDofTransformation->InvTransformPrimal(elementDisplacement);
|
|
}
|
|
|
|
if (compactificationDofTransformation != nullptr) {
|
|
compactificationDofTransformation->InvTransformPrimal(elementCompactification);
|
|
}
|
|
|
|
const mean_field::mapping::ElementDisplacementData displacementData =
|
|
mean_field::mapping::ElementDisplacementDataFromElementVDofs(displacementElement, elementDisplacement);
|
|
|
|
const mean_field::mapping::ElementCompactificationData compactificationData(
|
|
compactificationElement, elementCompactification
|
|
);
|
|
|
|
const mean_field::mapping::ElementMappingData mappingData{
|
|
.displacement = displacementData, .compactification = compactificationData
|
|
};
|
|
|
|
const mean_field::quadrature::Query query =
|
|
EnthalpyField::make_query<mean_field::field::Enthalpy::Form::PressureIntegral>(
|
|
mean_field::quadrature::QuadratureRole::diagnostic, transformation->OrderW(),
|
|
std::array<int, 1>{pressureExtraOrder}, mean_field::utils::DOMAINS::STELLAR,
|
|
mean_field::quadrature::MappingKind::general
|
|
);
|
|
|
|
const mean_field::quadrature::MfemRule rule =
|
|
f.quadratureFactory->get(query, transformation->GetGeometryType());
|
|
|
|
MFEM_VERIFY(rule.integration_rule != nullptr, "The pressure-integral quadrature rule is null.");
|
|
|
|
enthalpyShape.SetSize(enthalpyElement.GetDof());
|
|
|
|
for (int quadratureIndex = 0; quadratureIndex < rule.integration_rule->GetNPoints(); ++quadratureIndex) {
|
|
const mfem::IntegrationPoint &integrationPoint = rule.integration_rule->IntPoint(quadratureIndex);
|
|
|
|
transformation->SetIntPoint(&integrationPoint);
|
|
|
|
const mean_field::mapping::MappingStatus mappingStatus = domainMapper.EvaluateVolume(
|
|
mappingData, *transformation, integrationPoint, workspace, mappingContext
|
|
);
|
|
|
|
MFEM_VERIFY(
|
|
mappingStatus == mean_field::mapping::MappingStatus::valid,
|
|
"Stateless mapping failed in the "
|
|
"independent pressure integral. Element: "
|
|
<< elementId << ", attribute: " << transformation->Attribute
|
|
<< ", quadrature point: " << quadratureIndex << ", status: " << static_cast<int>(mappingStatus)
|
|
);
|
|
|
|
enthalpyElement.CalcShape(integrationPoint, enthalpyShape);
|
|
|
|
const double enthalpyValue = elementEnthalpy * enthalpyShape;
|
|
|
|
const double pressureValue = mean_field::eos::evaluate<mean_field::eos::quantity::Pressure>(
|
|
barotrope, mean_field::eos::SpecificEnthalpyValue{enthalpyValue}
|
|
)
|
|
.value();
|
|
|
|
const double contribution = pressureValue * mappingContext.quadrature.weight;
|
|
|
|
MFEM_VERIFY(
|
|
std::isfinite(pressureValue) && std::isfinite(contribution), "The independent pressure integral "
|
|
"encountered a non-finite value."
|
|
);
|
|
|
|
localPressureIntegral += contribution;
|
|
}
|
|
}
|
|
|
|
double globalPressureIntegral = 0.0;
|
|
|
|
MPI_Allreduce(&localPressureIntegral, &globalPressureIntegral, 1, MPI_DOUBLE, MPI_SUM, f.mesh->GetComm());
|
|
|
|
return globalPressureIntegral;
|
|
}
|
|
} // namespace pressure_force_kernel_test_utils
|
|
|
|
TEST_CASE(
|
|
"Pressure Force Residual Vanishes For Zero Enthalpy",
|
|
tags::barotrope &tags::pressure &tags::kernels &tags::integration
|
|
) {
|
|
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 mean_field::eos::Polytrope barotrope(3.0, 0.25);
|
|
|
|
mfem::Vector enthalpyTrue(f.enthalpyFes->GetTrueVSize());
|
|
enthalpyTrue = 0.0;
|
|
|
|
const mfem::Vector displacementTrue = pressure_force_kernel_test_utils::make_zero_displacement(f);
|
|
|
|
mfem::Vector residualTrue;
|
|
|
|
mean_field::operators::kernels::apply_pressure_force_residual(
|
|
f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementTrue, residualTrue
|
|
);
|
|
|
|
REQUIRE(residualTrue.Size() == f.displacementFes->GetTrueVSize());
|
|
|
|
const double residualNorm = gravity_prepared_test_utils::global_norm(residualTrue, f.mesh->GetComm());
|
|
|
|
CHECK(residualNorm == 0.0);
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Pressure Force Residual Excludes Vacuum Enthalpy Exactly",
|
|
tags::barotrope &tags::pressure &tags::kernels &tags::integration
|
|
) {
|
|
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 mean_field::eos::Polytrope barotrope(3.0, 0.25);
|
|
|
|
const mfem::Vector enthalpyTrue = pressure_force_kernel_test_utils::make_vacuum_only_enthalpy(f);
|
|
|
|
const double enthalpyNorm = gravity_prepared_test_utils::global_norm(enthalpyTrue, f.mesh->GetComm());
|
|
|
|
/*
|
|
* Ensure this is a real exclusion test rather than another
|
|
* all-zero-input test.
|
|
*/
|
|
REQUIRE(enthalpyNorm > 0.0);
|
|
|
|
const mfem::Vector displacementTrue = pressure_force_kernel_test_utils::make_zero_displacement(f);
|
|
|
|
mfem::Vector residualTrue;
|
|
|
|
mean_field::operators::kernels::apply_pressure_force_residual(
|
|
f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementTrue, residualTrue
|
|
);
|
|
|
|
REQUIRE(residualTrue.Size() == f.displacementFes->GetTrueVSize());
|
|
|
|
const double residualNorm = gravity_prepared_test_utils::global_norm(residualTrue, f.mesh->GetComm());
|
|
|
|
CHECK(residualNorm == 0.0);
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Pressure Force Residual Is Nonzero For Positive Stellar Pressure",
|
|
tags::barotrope &tags::pressure &tags::kernels &tags::integration
|
|
) {
|
|
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 mean_field::eos::Polytrope barotrope(3.0, 0.25);
|
|
|
|
/*
|
|
* With n = 3 and K = 1/4:
|
|
*
|
|
* P(1) = 1/4.
|
|
*/
|
|
mfem::Vector enthalpyTrue(f.enthalpyFes->GetTrueVSize());
|
|
enthalpyTrue = 1.0;
|
|
|
|
const mfem::Vector displacementTrue = pressure_force_kernel_test_utils::make_zero_displacement(f);
|
|
|
|
mfem::Vector residualTrue;
|
|
|
|
mean_field::operators::kernels::apply_pressure_force_residual(
|
|
f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementTrue, residualTrue
|
|
);
|
|
|
|
const double residualNorm = gravity_prepared_test_utils::global_norm(residualTrue, f.mesh->GetComm());
|
|
|
|
INFO("Positive-pressure residual norm = " << residualNorm);
|
|
|
|
CHECK(std::isfinite(residualNorm));
|
|
|
|
CHECK(residualNorm > 100.0 * std::numeric_limits<double>::epsilon());
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Pressure Force Residual Does No Work Against Rigid Translations",
|
|
tags::barotrope &tags::pressure &tags::kernels &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());
|
|
|
|
REQUIRE(f.displacementFes->GetOrdering() == mfem::Ordering::byNODES);
|
|
|
|
const mean_field::eos::Polytrope barotrope(3.0, 0.25);
|
|
|
|
const mfem::Vector enthalpyTrue = pressure_force_kernel_test_utils::make_positive_asymmetric_enthalpy(f);
|
|
|
|
const mfem::Vector displacementTrue = pressure_force_kernel_test_utils::make_zero_displacement(f);
|
|
|
|
mfem::Vector residualTrue;
|
|
|
|
mean_field::operators::kernels::apply_pressure_force_residual(
|
|
f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementTrue, residualTrue
|
|
);
|
|
|
|
const double residualNorm = gravity_prepared_test_utils::global_norm(residualTrue, f.mesh->GetComm());
|
|
|
|
REQUIRE(residualNorm > 0.0);
|
|
|
|
const int dimension = f.mesh->Dimension();
|
|
|
|
for (int component = 0; component < dimension; ++component) {
|
|
const mfem::Vector translationTrue =
|
|
pressure_force_kernel_test_utils::make_component_test_field(f, component, -1);
|
|
|
|
const double translationNorm = gravity_prepared_test_utils::global_norm(translationTrue, f.mesh->GetComm());
|
|
|
|
const double translationWork =
|
|
pressure_force_kernel_test_utils::global_dot(translationTrue, residualTrue, f.mesh->GetComm());
|
|
|
|
const double dotProductScale = std::fmax(residualNorm * translationNorm, 1.0);
|
|
|
|
CAPTURE(component, translationWork, dotProductScale);
|
|
|
|
CHECK(std::abs(translationWork) <= 5.0e-12 * dotProductScale);
|
|
}
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Pressure Force Residual Matches Independent Pressure Integral",
|
|
tags::barotrope &tags::pressure &tags::kernels &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());
|
|
|
|
REQUIRE(f.displacementFes->GetOrdering() == mfem::Ordering::byNODES);
|
|
|
|
const mean_field::eos::Polytrope barotrope(3.0, 0.25);
|
|
|
|
const mfem::Vector enthalpyTrue = pressure_force_kernel_test_utils::make_positive_asymmetric_enthalpy(f);
|
|
|
|
const mfem::Vector displacementTrue = pressure_force_kernel_test_utils::make_zero_displacement(f);
|
|
|
|
mfem::Vector residualTrue;
|
|
|
|
mean_field::operators::kernels::apply_pressure_force_residual(
|
|
f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementTrue, residualTrue
|
|
);
|
|
|
|
const int dimension = f.mesh->Dimension();
|
|
|
|
REQUIRE(dimension == 3);
|
|
|
|
mfem::DenseMatrix virtualWork(dimension, dimension);
|
|
|
|
for (int component = 0; component < dimension; ++component) {
|
|
for (int coordinate = 0; coordinate < dimension; ++coordinate) {
|
|
const mfem::Vector affineTestTrue =
|
|
pressure_force_kernel_test_utils::make_component_test_field(f, component, coordinate);
|
|
|
|
virtualWork(component, coordinate) =
|
|
pressure_force_kernel_test_utils::global_dot(affineTestTrue, residualTrue, f.mesh->GetComm());
|
|
}
|
|
}
|
|
|
|
const double pressureIntegral = pressure_force_kernel_test_utils::integrate_pressure(
|
|
f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementTrue
|
|
);
|
|
|
|
REQUIRE(std::isfinite(pressureIntegral));
|
|
|
|
REQUIRE(pressureIntegral > 100.0 * std::numeric_limits<double>::epsilon());
|
|
|
|
double meanDiagonalWork = 0.0;
|
|
|
|
for (int component = 0; component < dimension; ++component) {
|
|
meanDiagonalWork += virtualWork(component, component);
|
|
}
|
|
|
|
meanDiagonalWork /= static_cast<double>(dimension);
|
|
|
|
const double comparisonTolerance = 1.0e-6 * std::abs(pressureIntegral);
|
|
|
|
INFO("Independent pressure integral = " << pressureIntegral);
|
|
|
|
INFO("Expected diagonal virtual work = " << -pressureIntegral);
|
|
|
|
INFO("Mean diagonal virtual work = " << meanDiagonalWork);
|
|
|
|
INFO("Comparison tolerance = " << comparisonTolerance);
|
|
|
|
/*
|
|
* This separate mean check gives a compact diagnostic if all three
|
|
* diagonal components drift together.
|
|
*/
|
|
CHECK(std::abs(meanDiagonalWork + pressureIntegral) <= comparisonTolerance);
|
|
|
|
for (int component = 0; component < dimension; ++component) {
|
|
for (int coordinate = 0; coordinate < dimension; ++coordinate) {
|
|
const double computedWork = virtualWork(component, coordinate);
|
|
|
|
const double expectedWork = component == coordinate ? -pressureIntegral : 0.0;
|
|
|
|
CAPTURE(component, coordinate, computedWork, expectedWork, pressureIntegral, comparisonTolerance);
|
|
|
|
CHECK(std::abs(computedWork - expectedWork) <= comparisonTolerance);
|
|
}
|
|
}
|
|
|
|
const double relativeMeanError = std::abs(meanDiagonalWork + pressureIntegral) / std::abs(pressureIntegral);
|
|
|
|
INFO("Relative mean diagonal error = " << relativeMeanError);
|
|
|
|
CHECK(relativeMeanError <= 1.0e-6);
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Pressure Force Residual Matches Deformed Pressure Volume Variation",
|
|
tags::barotrope &tags::pressure &tags::kernels &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 mean_field::eos::Polytrope barotrope(3.0, 0.25);
|
|
|
|
/*
|
|
* This field is positive but spatially nonuniform, so the test
|
|
* exercises a genuinely nonuniform pressure distribution.
|
|
*/
|
|
const mfem::Vector enthalpyTrue =
|
|
pressure_force_kernel_test_utils::make_deterministic_vector(f.enthalpyFes->GetTrueVSize(), 0.37);
|
|
|
|
/*
|
|
* make_displacement() contains anisotropic diagonal terms and
|
|
* quadratic cross terms. A scale of 0.67 therefore provides a
|
|
* nonzero, nonspherical, valid base geometry.
|
|
*/
|
|
const mfem::Vector baseDisplacementTrue = gravity_prepared_test_utils::make_displacement(f, 0.67);
|
|
|
|
/*
|
|
* Differentiate along the same smooth deformation family. Thus
|
|
*
|
|
* d(epsilon) = (0.67 + epsilon) d_shape.
|
|
*
|
|
* This gives a controlled geometry path while still evaluating
|
|
* the derivative at a genuinely deformed base state.
|
|
*/
|
|
const mfem::Vector displacementVariationTrue = gravity_prepared_test_utils::make_displacement(f, 1.0);
|
|
|
|
const double baseDisplacementNorm =
|
|
gravity_prepared_test_utils::global_norm(baseDisplacementTrue, f.mesh->GetComm());
|
|
|
|
const double variationNorm = gravity_prepared_test_utils::global_norm(displacementVariationTrue, f.mesh->GetComm());
|
|
|
|
REQUIRE(baseDisplacementNorm > 100.0 * std::numeric_limits<double>::epsilon());
|
|
|
|
REQUIRE(variationNorm > 100.0 * std::numeric_limits<double>::epsilon());
|
|
|
|
mfem::Vector residualTrue;
|
|
|
|
mean_field::operators::kernels::apply_pressure_force_residual(
|
|
f, *f.domainMapperStateless, barotrope, enthalpyTrue, baseDisplacementTrue, residualTrue
|
|
);
|
|
|
|
REQUIRE(residualTrue.Size() == f.displacementFes->GetTrueVSize());
|
|
|
|
const double residualWork =
|
|
pressure_force_kernel_test_utils::global_dot(displacementVariationTrue, residualTrue, f.mesh->GetComm());
|
|
|
|
REQUIRE(std::isfinite(residualWork));
|
|
|
|
REQUIRE(std::abs(residualWork) > 100.0 * std::numeric_limits<double>::epsilon());
|
|
|
|
/*
|
|
* The relatively broad initial sweep lets us see the expected
|
|
* centered-difference convergence before reaching the quadrature
|
|
* and representation plateau.
|
|
*/
|
|
constexpr std::array<double, 4> differenceSteps{1.0e-2, 5.0e-3, 2.5e-3, 1.25e-3};
|
|
|
|
double bestRelativeDiscrepancy = std::numeric_limits<double>::infinity();
|
|
|
|
for (const double differenceStep : differenceSteps) {
|
|
mfem::Vector displacementPlus(baseDisplacementTrue);
|
|
|
|
mfem::Vector displacementMinus(baseDisplacementTrue);
|
|
|
|
displacementPlus.Add(differenceStep, displacementVariationTrue);
|
|
|
|
displacementMinus.Add(-differenceStep, displacementVariationTrue);
|
|
|
|
const double pressureIntegralPlus = pressure_force_kernel_test_utils::integrate_pressure(
|
|
f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementPlus
|
|
);
|
|
|
|
const double pressureIntegralMinus = pressure_force_kernel_test_utils::integrate_pressure(
|
|
f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementMinus
|
|
);
|
|
|
|
REQUIRE(std::isfinite(pressureIntegralPlus));
|
|
REQUIRE(std::isfinite(pressureIntegralMinus));
|
|
|
|
const double pressureVolumeDerivative = (pressureIntegralPlus - pressureIntegralMinus) / (2.0 * differenceStep);
|
|
|
|
REQUIRE(std::isfinite(pressureVolumeDerivative));
|
|
|
|
double comparisonScale = std::abs(residualWork);
|
|
|
|
if (std::abs(pressureVolumeDerivative) > comparisonScale) {
|
|
comparisonScale = std::abs(pressureVolumeDerivative);
|
|
}
|
|
|
|
REQUIRE(comparisonScale > 100.0 * std::numeric_limits<double>::epsilon());
|
|
|
|
const double absoluteDiscrepancy = std::abs(residualWork + pressureVolumeDerivative);
|
|
|
|
const double relativeDiscrepancy = absoluteDiscrepancy / comparisonScale;
|
|
|
|
if (relativeDiscrepancy < bestRelativeDiscrepancy) {
|
|
bestRelativeDiscrepancy = relativeDiscrepancy;
|
|
}
|
|
|
|
INFO("Difference step = " << differenceStep);
|
|
|
|
INFO("Pressure residual work = " << residualWork);
|
|
|
|
INFO("Pressure-volume derivative = " << pressureVolumeDerivative);
|
|
|
|
INFO("Residual work plus derivative = " << residualWork + pressureVolumeDerivative);
|
|
|
|
INFO("Relative discrepancy = " << relativeDiscrepancy);
|
|
|
|
/*
|
|
* The signs must be opposite because the implemented pressure
|
|
* force is the negative variation of the pressure-volume
|
|
* functional.
|
|
*/
|
|
CHECK(residualWork * pressureVolumeDerivative < 0.0);
|
|
}
|
|
|
|
INFO("Best pressure-volume relative discrepancy = " << bestRelativeDiscrepancy);
|
|
|
|
/*
|
|
* This is intentionally a provisional but meaningful threshold.
|
|
* We will tighten it after measuring the convergence plateau.
|
|
*/
|
|
CHECK(bestRelativeDiscrepancy < 1.0e-8);
|
|
}
|