Snapshot optimized solver and audit diagnostics before numerical experiments

This commit is contained in:
2026-09-16 14:34:06 -04:00
parent 75cc638739
commit 80cc3d83d7
64 changed files with 11969 additions and 267 deletions

View File

@@ -1,5 +1,9 @@
#include <algorithm>
#include <array>
#include <algorithm>
#include <cmath>
#include <memory>
#include <limits>
#include <type_traits>
#include <utility>
@@ -260,3 +264,295 @@ TEST_CASE(
CheckMatrixExactly(retained->GetValues(0), firstExpected);
CheckMatrixExactly(retained->GetValues(retained->GetPointCount() - 1), lastExpected);
}
TEST_CASE("Tensor reference evaluation matches native H1 hex values and gradients",
tags::unit &tags::quadrature) {
for (const int order : {1, 2, 4}) {
for (const int basis : {mfem::BasisType::GaussLobatto, mfem::BasisType::ClosedUniform}) {
mfem::H1_FECollection collection(order, 3, basis);
const auto &element = *collection.FiniteElementForGeometry(mfem::Geometry::CUBE);
const auto rule = CopyRule(mfem::IntRules.Get(mfem::Geometry::CUBE, 2 * order + 3));
const TableCache cache;
const auto table = cache.GetScalarTable(element, rule);
REQUIRE(table->HasTensorEvaluation3D());
mfem::DenseMatrix dofs(element.GetDof(), 3), actual;
mfem::Vector workspace;
for (int c = 0; c < 3; ++c) for (int i = 0; i < element.GetDof(); ++i)
dofs(i, c) = std::sin(0.37 * (i + 1) + c) + 0.21 * std::cos(0.13 * i - c);
table->EvaluateTensor3D(dofs, actual, workspace);
REQUIRE(actual.Height() == rule.GetNPoints());
REQUIRE(actual.Width() == 12);
for (int q = 0; q < rule.GetNPoints(); ++q) for (int c = 0; c < 3; ++c) {
for (int derivative = 0; derivative < 4; ++derivative) {
double expected = 0.0;
for (int i = 0; i < element.GetDof(); ++i)
expected += dofs(i, c) * (derivative == 0 ? table->GetValues()(q, i) :
table->GetGradients(q)(i, derivative - 1));
CHECK(std::abs(actual(q, 4 * c + derivative) - expected) <
2.0e-12 * std::max(1.0, std::abs(expected)));
}
}
// Reused caller scratch must not retain a previous direction.
dofs = 0.0;
table->EvaluateTensor3D(dofs, actual, workspace);
for (int c = 0; c < actual.Width(); ++c) for (int q = 0; q < actual.Height(); ++q)
CHECK(actual(q, c) == 0.0);
}
}
}
TEST_CASE("Tensor reference evaluation rejects non-Cartesian rules and owns its data",
tags::unit &tags::quadrature) {
std::shared_ptr<const ScalarTable> retained;
mfem::DenseMatrix input, expected;
{
mfem::H1_FECollection collection(3, 3);
const auto &element = *collection.FiniteElementForGeometry(mfem::Geometry::CUBE);
auto rule = CopyRule(mfem::IntRules.Get(mfem::Geometry::CUBE, 7));
const TableCache cache;
retained = cache.GetScalarTable(element, rule);
REQUIRE(retained->HasTensorEvaluation3D());
input.SetSize(element.GetDof(), 1);
input = 1.0;
mfem::Vector scratch;
retained->EvaluateTensor3D(input, expected, scratch);
rule.IntPoint(1).y += 0.001;
CHECK_FALSE(cache.GetScalarTable(element, rule)->HasTensorEvaluation3D());
const auto &tet = *collection.FiniteElementForGeometry(mfem::Geometry::TETRAHEDRON);
CHECK_FALSE(cache.GetScalarTable(tet, mfem::IntRules.Get(mfem::Geometry::TETRAHEDRON, 7))
->HasTensorEvaluation3D());
}
mfem::DenseMatrix actual;
mfem::Vector scratch;
retained->EvaluateTensor3D(input, actual, scratch);
CheckMatrixExactly(actual, expected);
}
TEST_CASE("Tensor Reference Transposes Match Dense Assembly And Adjoint Products", tags::unit &tags::quadrature) {
for (int order : {1, 2, 4}) {
mfem::H1_FECollection h1(order, 3);
const auto &fe = *h1.FiniteElementForGeometry(mfem::Geometry::CUBE);
const auto &rule = mfem::IntRules.Get(mfem::Geometry::CUBE, 2*order+3);
TableCache cache;
auto table = cache.GetScalarTable(fe, rule);
REQUIRE(table->HasTensorEvaluation3D());
mfem::DenseMatrix dual(rule.GetNPoints(), 3), actual;
for (int c = 0; c < 3; ++c) for (int q = 0; q < dual.Height(); ++q)
dual(q,c) = std::sin(0.17*(q+1)*(c+1));
mfem::Vector workspace, expected(fe.GetDof());
for (int repeat = 0; repeat < 2; ++repeat) {
if (repeat) dual = 0.0;
table->ApplyTransposeTensor3D(dual, actual, workspace);
for (int c = 0; c < 3; ++c) {
table->GetValues().MultTranspose(dual.GetColumn(c), expected.GetData());
for (int i = 0; i < fe.GetDof(); ++i)
CHECK(std::abs(actual(i,c)-expected(i)) <= 2.0e-12 * std::max(1.0, expected.Normlinf()));
}
}
}
for (int order : {0, 1, 3}) for (int basis : {mfem::BasisType::GaussLegendre, mfem::BasisType::IntegratedGLL}) {
CAPTURE(order, basis);
mfem::RT_FECollection rt(order, 3, mfem::BasisType::GaussLobatto, basis);
const auto &fe = *rt.FiniteElementForGeometry(mfem::Geometry::CUBE);
auto rule = CopyRule(mfem::IntRules.Get(mfem::Geometry::CUBE, 2*order+5));
TableCache cache;
auto table = cache.GetVectorTable(fe, rule);
REQUIRE(table->HasTensorEvaluation3D());
mfem::Vector dofs(fe.GetDof()), assembled, expected(fe.GetDof()), point(3), workspace;
mfem::DenseMatrix values, dual(rule.GetNPoints(), 3);
for (int i = 0; i < dofs.Size(); ++i) dofs(i) = std::sin(0.53*(i+1));
for (int c = 0; c < 3; ++c) for (int q = 0; q < dual.Height(); ++q) dual(q,c) = std::cos(0.31*(q+1)+c);
for (int repeat = 0; repeat < 2; ++repeat) {
if (repeat) { dofs = 0.0; dual = 0.0; }
table->EvaluateTensor3D(dofs, values, workspace);
expected = 0.0;
double primalProduct = 0.0;
for (int q = 0; q < rule.GetNPoints(); ++q) {
table->GetValues(q).MultTranspose(dofs, point);
for (int c = 0; c < 3; ++c) {
CHECK(std::abs(values(q,c)-point(c)) <= 2.0e-11 * std::max(1.0, point.Normlinf()));
primalProduct += values(q,c)*dual(q,c);
point(c) = dual(q,c);
}
table->GetValues(q).AddMult(point, expected);
}
table->ApplyTransposeTensor3D(dual, assembled, workspace);
mfem::Vector difference(assembled); difference -= expected;
CHECK(difference.Norml2() <= 2.0e-11 * std::max(1.0, expected.Norml2()));
CHECK(std::abs(primalProduct - dofs*assembled) <= 2.0e-11 * std::max(1.0, std::abs(primalProduct)));
}
rule.IntPoint(rule.GetNPoints()-1).x += 0.017;
CHECK_FALSE(cache.GetVectorTable(fe, rule)->HasTensorEvaluation3D());
}
}
TEST_CASE("RT Tensor Plans Own Their Factors After Cache And FE Destruction", tags::unit &tags::quadrature) {
std::shared_ptr<const VectorTable> table;
mfem::Vector dofs, expected;
mfem::DenseMatrix values, dual;
{
mfem::RT_FECollection rt(2, 3);
TableCache cache;
auto rule = CopyRule(mfem::IntRules.Get(mfem::Geometry::CUBE, 9));
const auto &fe = *rt.FiniteElementForGeometry(mfem::Geometry::CUBE);
table = cache.GetVectorTable(fe, rule);
REQUIRE(table->HasTensorEvaluation3D());
dofs.SetSize(fe.GetDof()); dofs = 0.3;
dual.SetSize(rule.GetNPoints(), 3); dual = 0.17;
mfem::Vector workspace;
table->ApplyTransposeTensor3D(dual, expected, workspace);
}
mfem::Vector workspace, actual;
table->EvaluateTensor3D(dofs, values, workspace);
table->ApplyTransposeTensor3D(dual, actual, workspace);
actual -= expected;
CHECK(actual.Norml2() == 0.0);
}
TEST_CASE("L2 Tensor Values And Transposes Match Dense Reference", tags::unit &tags::quadrature) {
for (int order : {0, 1, 3}) for (int basis : {mfem::BasisType::GaussLegendre, mfem::BasisType::OpenUniform}) {
mfem::L2_FECollection collection(order, 3, basis);
const auto &element = *collection.FiniteElementForGeometry(mfem::Geometry::CUBE);
const auto &rule = mfem::IntRules.Get(mfem::Geometry::CUBE, 2*order+3);
TableCache cache;
const auto table = cache.GetScalarTable(element, rule);
REQUIRE(table->HasTensorEvaluation3D());
mfem::Vector dofs(element.GetDof()), dual(rule.GetNPoints()), values, expected(rule.GetNPoints()),
transpose, expectedTranspose(element.GetDof()), workspace;
for (int i = 0; i < dofs.Size(); ++i) dofs(i) = std::sin(0.37*(i+1));
for (int q = 0; q < dual.Size(); ++q) dual(q) = std::cos(0.19*(q+1));
table->EvaluateValuesTensor3D(dofs, values, workspace);
table->GetValues().Mult(dofs, expected);
mfem::Vector difference(values); difference -= expected;
CHECK(difference.Norml2() <= 2.0e-12*std::max(1.0, expected.Norml2()));
table->ApplyTransposeTensor3D(dual, transpose, workspace);
table->GetValues().MultTranspose(dual, expectedTranspose);
difference = transpose; difference -= expectedTranspose;
CHECK(difference.Norml2() <= 2.0e-12*std::max(1.0, expectedTranspose.Norml2()));
CHECK(std::abs((values*dual) - (dofs*transpose)) <= 2.0e-12*std::max(1.0, std::abs(values*dual)));
dofs = 0.0; dual = 0.0;
table->EvaluateValuesTensor3D(dofs, values, workspace);
table->ApplyTransposeTensor3D(dual, transpose, workspace);
CHECK(values.Norml2() == 0.0);
CHECK(transpose.Norml2() == 0.0);
}
}
TEST_CASE("Geometry Inspector Reuses And Invalidates Fixed Geometry", tags::unit &tags::geometry) {
for (const auto ordering : {mfem::Ordering::byNODES, mfem::Ordering::byVDIM}) {
auto serial = mfem::Mesh::MakeCartesian3D(2, 1, 1, mfem::Element::HEXAHEDRON);
serial.SetCurvature(2);
mfem::ParMesh mesh(MPI_COMM_WORLD, serial);
mfem::H1_FECollection collection(2, 3);
mfem::ParFiniteElementSpace space(&mesh, &collection, 3, ordering);
mfem::ParGridFunction field(&space);
mfem::VectorFunctionCoefficient displacement(3, [](const mfem::Vector &x, mfem::Vector &u) {
u.SetSize(3);
u(0) = 0.08*x(0) + 0.02*x(1)*x(2);
u(1) = -0.03*x(1) + 0.01*x(0)*x(2);
u(2) = 0.04*x(2);
});
field.ProjectCoefficient(displacement);
mfem::Vector state; field.GetTrueDofs(state);
mean_field::fem::PreparedGeometryInspector inspector;
auto compare = [&] {
const double fast = inspector.Inspect(space, state);
const double reference = inspector.Inspect(space, state, true);
CHECK(std::isfinite(fast));
CHECK(std::abs(fast-reference) <= 2.0e-12*std::max(1.0, std::abs(reference)));
return fast;
};
compare();
CHECK(inspector.GetTensorElementCount() == mesh.GetNE());
const auto builds = inspector.GetGeometryBuildCount();
state *= 0.5;
const double before = compare();
CHECK(inspector.GetGeometryBuildCount() == builds);
// Same FE/mesh topology, changed curved computational coordinates.
mfem::VectorFunctionCoefficient stretch(3, [](const mfem::Vector &x, mfem::Vector &y) {
y = x; y(0) = 1.2*x(0); y(1) += 0.04*x(0)*x(2);
});
mesh.Transform(stretch);
const double after = compare();
CHECK(std::abs(after-before) > 1.0e-6);
CHECK(inspector.GetGeometryBuildCount() == builds + mesh.GetNE());
inspector.ClearCache();
CHECK(std::abs(compare()-after) < 2.0e-14);
CHECK(inspector.GetGeometryBuildCount() == builds + 2*mesh.GetNE());
mfem::VectorFunctionCoefficient folded(3, [](const mfem::Vector &x, mfem::Vector &u) {
u.SetSize(3); u = 0.0; u(0) = -2.0*x(0);
});
field.ProjectCoefficient(folded); field.GetTrueDofs(state);
CHECK(compare() < 0.0);
if (state.Size()) state(0) = std::numeric_limits<double>::quiet_NaN();
CHECK_FALSE(std::isfinite(inspector.Inspect(space, state)));
CHECK_FALSE(std::isfinite(inspector.Inspect(space, state, true)));
}
}
TEST_CASE("Geometry Inspector Preserves Tetrahedron Fallback", tags::unit &tags::geometry) {
auto serial = mfem::Mesh::MakeCartesian3D(1, 1, 1, mfem::Element::TETRAHEDRON);
mfem::ParMesh mesh(MPI_COMM_WORLD, serial);
mfem::H1_FECollection collection(1, 3);
mfem::ParFiniteElementSpace space(&mesh, &collection, 3);
mfem::Vector state(space.GetTrueVSize()); state = 0.0;
mean_field::fem::PreparedGeometryInspector inspector;
CHECK(inspector.Inspect(space, state) == 1.0);
CHECK(inspector.Inspect(space, state, true) == 1.0);
CHECK(inspector.GetTensorElementCount() == 0);
CHECK(inspector.GetGeometryBuildCount() == 0);
}
TEST_CASE("Tensor Gradient Transposes Match Dense Assembly And Adjoint Products", tags::unit &tags::quadrature) {
const auto check = [](const mfem::FiniteElement &element, const mfem::IntegrationRule &rule) {
TableCache cache;
const auto table = cache.GetScalarTable(element, rule);
REQUIRE(table->HasTensorEvaluation3D());
constexpr int components = 2;
mfem::DenseMatrix dofs(element.GetDof(), components), dual(rule.GetNPoints(), 3*components),
values, actual, expected(element.GetDof(), components);
mfem::Vector workspace;
for (int c = 0; c < components; ++c) for (int i = 0; i < dofs.Height(); ++i)
dofs(i,c) = std::sin(0.37*(i+1)+c);
for (int c = 0; c < dual.Width(); ++c) for (int q = 0; q < dual.Height(); ++q)
dual(q,c) = std::cos(0.23*(q+1)+0.31*c);
for (int repeat = 0; repeat < 2; ++repeat) {
if (repeat) dual = 0.0;
table->EvaluateTensor3D(dofs, values, workspace);
table->ApplyGradientTransposeTensor3D(dual, actual, workspace);
expected = 0.0;
double forwardProduct = 0.0, transposeProduct = 0.0, scale = 1.0;
for (int c = 0; c < components; ++c) for (int q = 0; q < rule.GetNPoints(); ++q)
for (int a = 0; a < 3; ++a) {
forwardProduct += values(q,4*c+a+1)*dual(q,3*c+a);
for (int i = 0; i < element.GetDof(); ++i)
expected(i,c) += table->GetGradients(q)(i,a)*dual(q,3*c+a);
}
for (int c = 0; c < components; ++c) for (int i = 0; i < dofs.Height(); ++i)
scale = std::max(scale, std::abs(expected(i,c)));
for (int c = 0; c < components; ++c) for (int i = 0; i < dofs.Height(); ++i) {
CHECK(std::abs(actual(i,c)-expected(i,c)) <= 2.0e-11*scale);
transposeProduct += dofs(i,c)*actual(i,c);
if (repeat) CHECK(actual(i,c) == 0.0);
}
CHECK(std::abs(forwardProduct-transposeProduct) <=
2.0e-11*std::max(1.0, std::abs(forwardProduct)));
}
};
for (int order : {1, 2, 4}) {
CAPTURE(order);
mfem::H1_FECollection collection(order, 3);
check(*collection.FiniteElementForGeometry(mfem::Geometry::CUBE),
mfem::IntRules.Get(mfem::Geometry::CUBE, 2*order+3));
}
for (int order : {0, 1, 3}) for (int basis : {mfem::BasisType::GaussLegendre, mfem::BasisType::OpenUniform}) {
CAPTURE(order, basis);
mfem::L2_FECollection collection(order, 3, basis);
check(*collection.FiniteElementForGeometry(mfem::Geometry::CUBE),
mfem::IntRules.Get(mfem::Geometry::CUBE, 2*order+3));
}
}

View File

@@ -1,3 +1,4 @@
#include <algorithm>
#include <array>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
@@ -102,6 +103,43 @@ namespace {
}
} // namespace
TEST_CASE("Kelvin Prepared Jacobian Factors Match Full Variation", tags::unit &tags::mapping &tags::kelvin) {
mapping::compactification::KelvinCompactification kelvin({.r_star_ref = 1.3, .r_inf_ref = 3.1});
const auto position = make_vector(1.1, 0.2, -0.1);
const auto coordinateGradient = make_vector(0.2, 0.03, -0.01);
auto jacobian = make_identity();
jacobian(0, 1) = 0.07;
const auto direction = make_vector(-0.02, 0.07, 0.04);
auto directionJacobian = make_identity();
directionJacobian *= 0.03;
directionJacobian(1, 2) = -0.05;
for (const double coordinate : {0.0, 0.4, 0.93, 0.999}) {
const mapping::compactification::ExteriorMapInput input{
position, position, jacobian, coordinate, coordinateGradient};
mapping::compactification::ExteriorMapResult base;
REQUIRE(kelvin.Evaluate(input, base) == mapping::MappingStatus::valid);
mapping::compactification::ExteriorMapVariation variation;
REQUIRE(kelvin.EvaluateVariation(input, base, {direction, directionJacobian}, variation) == mapping::MappingStatus::valid);
double scale = 0.0;
mfem::Vector scaleGradient;
REQUIRE(kelvin.EvaluateJacobianVariationFactors(coordinate, coordinateGradient, scale, scaleGradient) == mapping::MappingStatus::valid);
for (int row = 0; row < 3; ++row) {
for (int column = 0; column < 3; ++column) {
const double expected = variation.mapping_jacobian_variation(row, column);
CHECK_THAT(scale * directionJacobian(row, column) + direction(row) * scaleGradient(column),
WithinAbs(expected, 1.0e-12 * std::max(1.0, std::abs(expected))));
}
}
}
double scale = 0.0;
mfem::Vector gradient;
CHECK(kelvin.EvaluateJacobianVariationFactors(1.0, coordinateGradient, scale, gradient) == mapping::MappingStatus::at_compactified_infinity);
CHECK(kelvin.EvaluateJacobianVariationFactors(-0.1, coordinateGradient, scale, gradient) == mapping::MappingStatus::outside_reference_domain);
auto invalid = coordinateGradient;
invalid(1) = std::numeric_limits<double>::quiet_NaN();
CHECK(kelvin.EvaluateJacobianVariationFactors(0.4, invalid, scale, gradient) == mapping::MappingStatus::non_finite_input);
}
TEST_CASE(
"Kelvin Compactification Validates Its Configuration",
tags::unit &tags::mapping &tags::kelvin
@@ -719,4 +757,4 @@ TEST_CASE(
check_vector(first_a.physical_position, second_a.physical_position, tolerance);
check_matrix(first_a.mapping_jacobian, second_a.mapping_jacobian, tolerance);
}
}

View File

@@ -2870,3 +2870,80 @@ TEST_CASE(
CHECK_THROWS_AS(evaluator.Refresh(), std::invalid_argument);
CHECK_THROWS_AS(evaluator.EvaluatePoint(*transformation, integration_point, context), std::invalid_argument);
}
TEST_CASE("Fixed Mesh Geometry Cache Reuses Factors And Invalidates On Mesh Updates", "[mapping][geometry-cache]") {
SingleElementFixture fixture;
mapping::DomainMapper mapper({.dimension = dimension, .vacuum_element_attribute = 3}, make_kelvin_compactification());
mapping::DomainMapper::Workspace workspace(dimension);
const auto &rule = mfem::IntRules.Get(mfem::Geometry::CUBE, 5);
const auto &point = rule.IntPoint(0);
auto zero = fixture.MakeZeroElementDofs();
mapping::ElementDisplacementData displacement(fixture.GetElement(), zero);
ElementMappingDataOwner data(displacement);
auto compare = [&](const mfem::IntegrationPoint &ip) {
auto &transformation = *fixture.mesh.GetElementTransformation(0);
mapping::VolumeMappingContext fast, reference;
mapper.SetGeometryCacheEnabled(true);
REQUIRE(mapper.EvaluateVolume(data.Get(), transformation, ip, workspace, fast) == mapping::MappingStatus::valid);
mapper.SetGeometryCacheEnabled(false);
REQUIRE(mapper.EvaluateVolume(data.Get(), transformation, ip, workspace, reference) == mapping::MappingStatus::valid);
check_vector(fast.mapping.physical_position, reference.mapping.physical_position);
check_matrix(fast.mapping.mapping_jacobian, reference.mapping.mapping_jacobian);
check_matrix(fast.quadrature.J_inv, reference.quadrature.J_inv);
CHECK_THAT(fast.quadrature.weight, WithinAbs(reference.quadrature.weight, 1.0e-12));
mapper.SetGeometryCacheEnabled(true);
return fast.quadrature.weight;
};
const double originalWeight = compare(point);
CHECK(mapper.GetGeometryCacheStatistics().misses == 1);
compare(point);
CHECK(mapper.GetGeometryCacheStatistics().hits == 1);
auto changedPoint = point;
changedPoint.x += 0.013;
compare(changedPoint);
CHECK(mapper.GetGeometryCacheStatistics().points == 2);
changedPoint.weight *= 0.5;
compare(changedPoint);
CHECK(mapper.GetGeometryCacheStatistics().points == 2); // weights are supplied by the caller
fixture.mesh.Transform([](const mfem::Vector &x, mfem::Vector &y) {
y = x; y(0) *= 1.2; y(1) += 0.17;
});
const double newWeight = compare(point);
CHECK(newWeight > originalWeight);
CHECK(mapper.GetGeometryCacheStatistics().points == 1);
CHECK(mapper.GetGeometryCacheStatistics().misses == 3);
mapper.ClearGeometryCache();
CHECK(mapper.GetGeometryCacheStatistics().points == 0);
compare(point);
CHECK(mapper.GetGeometryCacheStatistics().misses == 1);
}
TEST_CASE("Geometry Cache Does Not Freeze Displacement Or Compactification State", "[mapping][geometry-cache]") {
SingleElementFixture fixture;
mapping::DomainMapper mapper({.dimension = dimension, .vacuum_element_attribute = 3}, make_kelvin_compactification());
mapping::DomainMapper::Workspace workspace(dimension);
const auto &point = mfem::IntRules.Get(mfem::Geometry::CUBE, 3).IntPoint(0);
for (double scale : {0.0, 0.02, -0.01}) {
auto &transformation = *fixture.mesh.GetElementTransformation(0);
auto dofs = fixture.MakeZeroElementDofs(); dofs = scale;
mapping::ElementDisplacementData displacement(fixture.GetElement(), dofs);
auto compactificationDofs = make_constant_compactification_dofs(fixture.GetElement(), 0.1 + scale);
mapping::ElementCompactificationData compactification(fixture.GetElement(), compactificationDofs);
mapping::ElementMappingData data{displacement, compactification};
for (int attribute : {1, 3}) {
transformation.Attribute = attribute;
mapping::VolumeMappingContext fast, reference;
mapper.SetGeometryCacheEnabled(true);
const auto fastStatus = mapper.EvaluateVolume(data, transformation, point, workspace, fast);
mapper.SetGeometryCacheEnabled(false);
const auto referenceStatus = mapper.EvaluateVolume(data, transformation, point, workspace, reference);
REQUIRE(fastStatus == referenceStatus);
if (fastStatus == mapping::MappingStatus::valid) {
check_vector(fast.mapping.physical_position, reference.mapping.physical_position);
check_matrix(fast.mapping.mapping_jacobian, reference.mapping.mapping_jacobian);
check_matrix(fast.quadrature.J_inv, reference.quadrature.J_inv);
}
}
}
CHECK(mapper.GetGeometryCacheStatistics().points == 1);
}

View File

@@ -13,6 +13,7 @@
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
#include <mpi.h>
import mean_field;
import test_helpers;
@@ -997,3 +998,83 @@ TEST_CASE(
CHECK(physicalInverse.IsCurrent());
CHECK(scaledInverse.IsCurrent());
}
TEST_CASE("Direct Reference Mass Diagonals Match Full True DOF Assembly", "[normalization][diagonal][assembly]") {
using namespace mean_field;
auto args = test_utils::setup_args();
auto f = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.okay());
auto compare = [](const mfem::Vector &a, const mfem::Vector &b) {
REQUIRE(a.Size() == b.Size());
mfem::Vector difference(a); difference -= b;
CHECK(difference.Norml2() <= 2.0e-11 * std::max(1.0, b.Norml2()));
};
compare(normalization::AssembleReferenceHDivMassDiagonal(*f.gravityFluxFes),
normalization::AssembleReferenceHDivMassDiagonal(*f.gravityFluxFes, true));
for (auto *space : {f.gravityPotentialFes.get(), f.densityFes.get(), f.enthalpyFes.get()}) {
compare(normalization::AssembleReferenceScalarMassDiagonal(*space),
normalization::AssembleReferenceScalarMassDiagonal(*space, nullptr, true));
mfem::Array<int> marker(f.mesh->attributes.Max());
marker = 0; marker[f.mesh->GetAttribute(0)-1] = 1;
compare(normalization::AssembleReferenceScalarMassDiagonal(*space, &marker),
normalization::AssembleReferenceScalarMassDiagonal(*space, &marker, true));
}
auto surfaceMap = field::make_stellar_surface_scalar_dof_map(*f.surfaceDeformationFes);
// The stellar surface is a tagged internal interface, which an exterior-only
// boundary face restriction would omit even though volume diagonals pass.
using Schema = utils::domain::CoreEnvelopeVacuumDomainSchema;
constexpr int surfaceAttribute = Schema::boundary_attribute<utils::domain::StellarSurface>();
int localFaceCounts[2] = {0, 0}, globalFaceCounts[2] = {0, 0};
for (int b = 0; b < f.mesh->GetNBE(); ++b) {
if (f.mesh->GetBdrAttribute(b) != surfaceAttribute) continue;
++localFaceCounts[0];
const int face = f.mesh->GetBdrElementFaceIndex(b);
if (f.mesh->GetFaceInformation(face).IsOfFaceType(mfem::FaceType::Interior))
++localFaceCounts[1];
}
REQUIRE(MPI_Allreduce(localFaceCounts, globalFaceCounts, 2, MPI_INT, MPI_SUM,
f.surfaceDeformationFes->GetComm()) == MPI_SUCCESS);
REQUIRE(globalFaceCounts[0] > 0);
REQUIRE(globalFaceCounts[1] == globalFaceCounts[0]);
const auto surface = normalization::AssembleReferenceSurfaceMassDiagonal(f, surfaceMap);
const auto referenceSurface = normalization::AssembleReferenceSurfaceMassDiagonal(f, surfaceMap, true);
REQUIRE(surface.Size() == referenceSurface.Size());
for (int i = 0; i < surface.Size(); ++i) {
CAPTURE(i, surface(i), referenceSurface(i));
CHECK(std::isfinite(surface(i)));
CHECK(surface(i) > 0.0);
CHECK(std::isfinite(referenceSurface(i)));
CHECK(referenceSurface(i) > 0.0);
}
mfem::Vector difference(surface);
difference -= referenceSurface;
const double errorNorm = difference.Norml2(), referenceNorm = referenceSurface.Norml2();
const double localSquares[2] = {errorNorm * errorNorm, referenceNorm * referenceNorm};
double globalSquares[2] = {0.0, 0.0};
REQUIRE(MPI_Allreduce(localSquares, globalSquares, 2, MPI_DOUBLE, MPI_SUM,
f.surfaceDeformationFes->GetComm()) == MPI_SUCCESS);
REQUIRE(globalSquares[1] > 0.0);
CHECK(std::sqrt(globalSquares[0] / globalSquares[1]) <= 2.0e-11);
}
TEST_CASE("Nonconforming Reference Diagonals Retain Exact Assembled Cross Terms", "[normalization][diagonal][nonconforming]") {
using namespace mean_field;
auto serial = mfem::Mesh::MakeCartesian3D(2, 1, 1, mfem::Element::HEXAHEDRON);
serial.EnsureNCMesh();
mfem::Array<int> refine(1); refine[0] = 0;
serial.GeneralRefinement(refine);
REQUIRE(serial.Nonconforming());
mfem::ParMesh mesh(MPI_COMM_WORLD, serial);
mfem::H1_FECollection h1(2, 3);
mfem::RT_FECollection rt(1, 3);
mfem::ParFiniteElementSpace scalar(&mesh, &h1), flux(&mesh, &rt);
auto actual = normalization::AssembleReferenceScalarMassDiagonal(scalar);
auto expected = normalization::AssembleReferenceScalarMassDiagonal(scalar, nullptr, true);
actual -= expected;
CHECK(actual.Norml2() == 0.0);
actual = normalization::AssembleReferenceHDivMassDiagonal(flux);
expected = normalization::AssembleReferenceHDivMassDiagonal(flux, true);
actual -= expected;
CHECK(actual.Norml2() == 0.0);
}

View File

@@ -601,6 +601,15 @@ TEST_CASE(
CHECK(preparedOperator.IsPrepared());
CHECK(preparedOperator.GetResidualPreparationCount() == 2);
CHECK(preparedOperator.GetResidualApplicationCount() == 1);
mfem::Vector updatedResidual, referenceResidual;
preparedOperator.BuildResidual(updatedResidual);
CHECK(preparedOperator.PrepareReference().DidAnyWork());
preparedOperator.BuildResidual(referenceResidual);
CHECK(gravity_displacement_force_test_utils::relative_difference(
updatedResidual, referenceResidual, f.mesh->GetComm()) < 2.0e-12);
CHECK_FALSE(preparedOperator.PrepareReference().DidAnyWork());
CHECK(preparedOperator.Prepare().DidAnyWork());
CHECK_FALSE(preparedOperator.Prepare().DidAnyWork());
}
TEST_CASE(
@@ -665,6 +674,19 @@ TEST_CASE(
densityDirectionReduced, displacementDirectionReduced, gravityGradientDirectionReduced, completeAction
);
mfem::Vector retainedAction;
preparedOperator.ApplyCompleteJacobianActionReference(
densityDirectionReduced, displacementDirectionReduced, gravityGradientDirectionReduced, retainedAction);
CHECK(preparedOperator.GetFastElementCount() > 0);
CHECK(preparedOperator.GetTensorElementCount() == preparedOperator.GetFastElementCount());
CHECK(gravity_displacement_force_test_utils::relative_difference(
completeAction, retainedAction, f.mesh->GetComm()) < 2.0e-12);
preparedOperator.ApplyCompleteJacobianActionCachedReference(
densityDirectionReduced, displacementDirectionReduced, gravityGradientDirectionReduced, retainedAction);
CHECK(gravity_displacement_force_test_utils::relative_difference(
completeAction, retainedAction, f.mesh->GetComm()) < 2.0e-12);
mfem::Vector summedColumns(densityAction);
summedColumns += gravityAction;
summedColumns += displacementAction;

View File

@@ -312,6 +312,10 @@ TEST_CASE(
operation.ApplyCompleteJacobianAction(
reducedDensityDirection, reducedDisplacementDirection, angularVelocityDirection, completeAction
);
mfem::Vector preparedReference;
operation.ApplyCompleteJacobianActionReference(
reducedDensityDirection, reducedDisplacementDirection, angularVelocityDirection, preparedReference);
CHECK(angular_momentum_test_utils::relativeError(completeAction(0), preparedReference(0)) < 2.0e-11);
CHECK(
angular_momentum_test_utils::relativeError(
completeAction(0), densityAction(0) + geometryAction(0) + angularVelocityAction(0)
@@ -409,10 +413,26 @@ TEST_CASE(
operation.Prepare(0.52, dependencies);
const auto preparationCount = operation.GetPreparationCount();
const double moment = operation.GetMomentOfInertia();
CHECK(operation.GetJacobianRowPreparationCount() == 0);
const auto rhoDirection = gravityContext.GetDensityMap().gather(
angular_momentum_test_utils::projectDensityDirection(finiteElements, 0.67));
const auto uDirection = gravityContext.GetDisplacementMap().gather(
angular_momentum_test_utils::projectDisplacementDirection(finiteElements, -0.53));
auto checkRows = [&](std::uint64_t expectedBuilds) {
REQUIRE(operation.PrepareJacobianRows());
CHECK(operation.GetJacobianRowPreparationCount() == expectedBuilds);
mfem::Vector actual, reference;
operation.ApplyCompleteJacobianAction(rhoDirection, uDirection, -0.19, actual);
operation.ApplyCompleteJacobianActionReference(rhoDirection, uDirection, -0.19, reference);
CHECK(angular_momentum_test_utils::relativeError(actual(0), reference(0)) < 2.0e-11);
CHECK(operation.GetJacobianRowPreparationCount() == expectedBuilds);
};
checkRows(1);
const auto repeated = operation.Prepare(0.52, dependencies);
CHECK_FALSE(repeated.DidAnyWork());
CHECK(operation.GetPreparationCount() == preparationCount);
checkRows(1);
++gravityPotentialRevision;
angular_momentum_test_utils::prepareGravityContext(
@@ -420,6 +440,7 @@ TEST_CASE(
);
const auto unrelatedPotential = operation.Prepare(0.52, dependencies);
CHECK_FALSE(unrelatedPotential.DidAnyWork());
checkRows(1);
const double residualBeforeRotation = angular_momentum_test_utils::residual(operation);
++dependencies.rotation.revision;
@@ -429,6 +450,7 @@ TEST_CASE(
CHECK_FALSE(rotationOnly.refreshedDensity);
CHECK_FALSE(rotationOnly.refreshedGeometry);
CHECK(operation.GetMomentOfInertia() == moment);
checkRows(1);
CHECK(
angular_momentum_test_utils::residual(operation) - residualBeforeRotation ==
Catch::Approx((0.81 - 0.52) * moment).epsilon(3.0e-15)
@@ -443,6 +465,8 @@ TEST_CASE(
CHECK(densityOnly.refreshedDensity);
CHECK_FALSE(densityOnly.refreshedGeometry);
CHECK_FALSE(densityOnly.updatedAngularVelocity);
CHECK(operation.GetJacobianRowPreparationCount() == 1);
checkRows(2);
displacement = angular_momentum_test_utils::projectDisplacementDirection(finiteElements, 0.87);
++dependencies.displacement.revision;
@@ -453,6 +477,8 @@ TEST_CASE(
CHECK(geometryOnly.refreshedGeometry);
CHECK_FALSE(geometryOnly.refreshedDensity);
CHECK_FALSE(geometryOnly.updatedAngularVelocity);
CHECK(operation.GetJacobianRowPreparationCount() == 2);
checkRows(3);
}
TEST_CASE(
@@ -488,6 +514,8 @@ TEST_CASE(
const auto initial = operation.TryPrepare(angularVelocity, dependencies);
REQUIRE(initial.has_value());
const std::uint64_t initialPreparationCount = operation.GetPreparationCount();
REQUIRE(operation.PrepareJacobianRows());
CHECK(operation.GetJacobianRowPreparationCount() == 1);
const std::uint64_t successfulPreparations = initialPreparationCount;
@@ -513,6 +541,8 @@ TEST_CASE(
const auto recovered = operation.TryPrepare(angularVelocity, dependencies);
REQUIRE(recovered.has_value());
CHECK(operation.IsPrepared());
REQUIRE(operation.PrepareJacobianRows());
CHECK(operation.GetJacobianRowPreparationCount() == 2);
++dependencies.rotation.revision;
const auto nonFiniteAngularVelocity = operation.TryPrepare(std::numeric_limits<double>::quiet_NaN(), dependencies);
@@ -527,4 +557,6 @@ TEST_CASE(
++dependencies.rotation.revision;
REQUIRE(operation.TryPrepare(angularVelocity, dependencies).has_value());
CHECK(operation.IsPrepared());
REQUIRE(operation.PrepareJacobianRows());
CHECK(operation.GetJacobianRowPreparationCount() == 3);
}

View File

@@ -526,6 +526,18 @@ namespace prepared_barotropic_closure_test_utils {
const MPI_Comm communicator = f.mesh->GetComm();
mfem::Vector retained, enthalpyOnly, displacementOnly;
preparedOperator.MultReference(densityVariation, enthalpyVariation, displacementVariation, retained);
preparedOperator.ApplyEnthalpyJacobianAction(enthalpyVariation, enthalpyOnly);
preparedOperator.ApplyDisplacementJacobianAction(displacementVariation, displacementOnly);
CHECK(relative_error(preparedCompleteAction, retained, communicator) < 2.0e-12);
CHECK(relative_error(enthalpyOnly, referenceEnthalpyAction, communicator) < 2.0e-12);
CHECK(relative_error(displacementOnly, referenceDisplacementAction, communicator) < 2.0e-12);
preparedOperator.ApplyEnthalpyJacobianAction(zeroEnthalpy, enthalpyOnly);
preparedOperator.ApplyDisplacementJacobianAction(zeroDisplacement, displacementOnly);
CHECK(enthalpyOnly.Norml2() == 0.0);
CHECK(displacementOnly.Norml2() == 0.0);
CHECK(relative_error(preparedResidual, referenceResidual, communicator) < 2.0e-12);
CHECK(relative_error(preparedDensityAction, referenceDensityAction, communicator) < 2.0e-12);
CHECK(relative_error(preparedEnthalpyAction, referenceEnthalpyAction, communicator) < 2.0e-12);
@@ -831,6 +843,16 @@ namespace prepared_barotropic_closure_test_utils {
mfem::Vector preparedAction;
preparedOperator.BuildResidual(preparedResidual);
preparedOperator.Mult(densityVariation, enthalpyVariation, displacementVariation, preparedAction);
CHECK(preparedOperator.GetTensorElementCount() > 0);
mfem::Vector cached, tensorColumn, cachedColumn;
preparedOperator.MultCachedReference(densityVariation, enthalpyVariation, displacementVariation, cached);
CHECK(relative_error(preparedAction, cached, communicator) < 2.0e-11);
preparedOperator.ApplyEnthalpyJacobianAction(enthalpyVariation, tensorColumn);
preparedOperator.ApplyEnthalpyJacobianAction(enthalpyVariation, cachedColumn, false);
CHECK(relative_error(tensorColumn, cachedColumn, communicator) < 2.0e-11);
preparedOperator.ApplyDisplacementJacobianAction(displacementVariation, tensorColumn);
preparedOperator.ApplyDisplacementJacobianAction(displacementVariation, cachedColumn, false);
CHECK(relative_error(tensorColumn, cachedColumn, communicator) < 2.0e-11);
mfem::Vector fullReferenceResidual;
mean_field::operators::kernels::apply_barotropic_closure(

View File

@@ -69,6 +69,7 @@ TEST_CASE(
REQUIRE(f.okay());
operators::PreparedMappedGravitySourceOperator operation(f, *f.domainMapperStateless);
operators::PreparedMappedGravitySourceOperator oldWeights(f, *f.domainMapperStateless);
const mfem::Vector densityTrue = prepared_test::make_deterministic_vector(f.densityFes->GetTrueVSize(), 0.41);
const mfem::Vector density = operation.GetDensityMap().gather(densityTrue);
const mfem::Vector displacementTrue = prepared_test::make_displacement(f, 0.4);
@@ -81,6 +82,21 @@ TEST_CASE(
mfem::Vector baselineVariation;
operation.Mult(density, baselineAction);
operation.MultDisplacementVariationTrue(densityTrue, directionTrue, baselineVariation);
REQUIRE(operation.GetTensorVariationElementCount() > 0);
mfem::Vector pointwiseVariation, zeroDirection(directionTrue), zeroVariation;
operation.MultDisplacementVariationTrueReference(densityTrue, directionTrue, pointwiseVariation);
CHECK(prepared_test::relative_error(baselineVariation, pointwiseVariation, communicator) < 2.0e-12);
zeroDirection = 0.0;
operation.MultDisplacementVariationTrue(densityTrue, zeroDirection, zeroVariation);
CHECK(zeroVariation.Norml2() == 0.0);
operation.MultDisplacementVariationTrue(densityTrue, directionTrue, pointwiseVariation);
CHECK(prepared_test::relative_error(baselineVariation, pointwiseVariation, communicator) < 2.0e-14);
oldWeights.PrepareReference(displacement);
mfem::Vector oldAction, oldVariation;
oldWeights.Mult(density, oldAction);
oldWeights.MultDisplacementVariationTrue(densityTrue, directionTrue, oldVariation);
CHECK(prepared_test::relative_error(baselineAction, oldAction, communicator) < 2.0e-14);
CHECK(prepared_test::relative_error(baselineVariation, oldVariation, communicator) < 2.0e-14);
const mfem::Vector primalDisplacementTrue = prepared_test::make_displacement(f, 1.0);
operation.PreparePrimal(operation.GetDisplacementMap().gather(primalDisplacementTrue));

View File

@@ -1,6 +1,7 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <cmath>
#include <memory>
#include <mfem.hpp>
#include <stdexcept>
@@ -12,6 +13,27 @@ using Catch::Matchers::WithinAbs;
namespace prepared_test = gravity_prepared_test_utils;
namespace {
// Deliberately not a KelvinCompactification dynamic type: the prepared
// operator must keep arbitrary exterior maps on the general derivative.
class DelegatingExteriorMap final : public mapping::compactification::ExteriorDomainMap {
public:
explicit DelegatingExteriorMap(mapping::compactification::options::KelvinCompactificationOptions options)
: kelvin(options) {}
mapping::MappingStatus Evaluate(const mapping::compactification::ExteriorMapInput &input,
mapping::compactification::ExteriorMapResult &result) const override {
return kelvin.Evaluate(input, result);
}
mapping::MappingStatus EvaluateVariation(const mapping::compactification::ExteriorMapInput &input,
const mapping::compactification::ExteriorMapResult &result,
const mapping::compactification::ExteriorMapDirection &direction,
mapping::compactification::ExteriorMapVariation &variation) const override {
return kelvin.EvaluateVariation(input, result, direction, variation);
}
std::string_view GetName() const noexcept override { return "DelegatingExteriorMap"; }
private:
mapping::compactification::KelvinCompactification kelvin;
};
[[nodiscard]] mfem::Vector make_folding_displacement(const mean_field::fem::FEM &f) {
mfem::ParGridFunction field(f.displacementFes.get());
mfem::VectorFunctionCoefficient coefficient(
@@ -65,9 +87,15 @@ TEST_CASE(
const mfem::Vector displacementTrue = prepared_test::make_displacement(f, scale);
const mfem::Vector displacement = preparedOperator.GetDisplacementMap().gather(displacementTrue);
preparedOperator.Prepare(displacement);
CHECK(preparedOperator.GetFastVariationElementCount() == f.mesh->GetNE());
CHECK(preparedOperator.GetTensorVariationElementCount() == f.mesh->GetNE());
preparedOperator.MultDisplacementVariationTrue(first, direction, firstAction);
mfem::Vector referenceAction;
preparedOperator.MultDisplacementVariationTrueCachedReference(first, direction, referenceAction);
CHECK(prepared_test::relative_error(firstAction, referenceAction, communicator) < 2.0e-11);
preparedOperator.MultDisplacementVariationTrueReference(first, direction, referenceAction);
CHECK(prepared_test::relative_error(firstAction, referenceAction, communicator) < 2.0e-11);
operators::kernels::apply_mapped_hdiv_mass_variation(
f, *f.domainMapperStateless, first, displacementTrue, direction, referenceAction
);
@@ -85,6 +113,39 @@ TEST_CASE(
CHECK(preparedOperator.GetPreparationCount() == 2);
}
TEST_CASE("Prepared Hdiv Tangent Retains General Exterior Map Fallback",
tags::gravity_prepared_jacobian_accuracy) {
auto args = test_utils::setup_args();
auto f = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.okay());
mapping::DomainMapper mapper(args.domain_mapper_options, std::make_unique<DelegatingExteriorMap>(args.kelvin_options));
operators::PreparedMappedHDivMassOperator prepared(f, mapper);
const auto base = prepared_test::make_displacement(f, 0.7);
const auto direction = prepared_test::make_displacement(f, 0.43);
const auto flux = prepared_test::make_deterministic_vector(f.gravityFluxFes->GetTrueVSize(), 0.31);
prepared.Prepare(prepared.GetDisplacementMap().gather(base));
int stellarElements = 0;
for (int element = 0; element < f.mesh->GetNE(); ++element)
stellarElements += !mapper.IsCompactifiedElement(*f.mesh->GetElementTransformation(element));
CHECK(prepared.GetFastVariationElementCount() == stellarElements);
mfem::Vector actual, expected;
prepared.MultDisplacementVariationTrue(flux, direction, actual);
operators::kernels::apply_mapped_hdiv_mass_variation(f, mapper, flux, base, direction, expected);
CHECK(prepared_test::relative_error(actual, expected, f.mesh->GetComm()) < 2.0e-11);
prepared.PreparePrimal(prepared.GetDisplacementMap().gather(base));
CHECK_FALSE(prepared.HasVariationData());
CHECK(prepared.GetFastVariationElementCount() == 0);
CHECK(prepared.GetFastMassElementCount() == f.mesh->GetNE());
mfem::Vector primal, primalReference;
const auto reducedFlux = prepared.GetFluxMap().gather(flux);
prepared.Mult(reducedFlux, primal);
prepared.MultReference(reducedFlux, primalReference);
CHECK(prepared_test::relative_error(primal, primalReference, f.mesh->GetComm()) < 2.0e-11);
prepared.Prepare(prepared.GetDisplacementMap().gather(base));
prepared.MultDisplacementVariationTrue(flux, direction, actual);
CHECK(prepared_test::relative_error(actual, expected, f.mesh->GetComm()) < 2.0e-11);
}
TEST_CASE(
"Prepared Mapped Hdiv Mass Reports Invalid Candidate Geometry Without Unwinding",
tags::gravity_prepared_unit &tags::geometry
@@ -144,6 +205,13 @@ TEST_CASE(
mfem::Vector prepared_action;
prepared_operator.Mult(gravity_gradient, prepared_action);
CHECK(prepared_operator.GetFastMassElementCount() == f.mesh->GetNE());
mfem::Vector mfemAction;
prepared_operator.MultReference(gravity_gradient, mfemAction);
CHECK(prepared_test::relative_error(prepared_action, mfemAction, communicator) < 2.0e-11);
// Reference calls must restore the optimized mode and leave PA data intact.
prepared_operator.Mult(gravity_gradient, mfemAction);
CHECK(prepared_test::relative_error(prepared_action, mfemAction, communicator) < 2.0e-14);
mfem::Vector reference_action_true;
operators::kernels::apply_mapped_hdiv_mass(
f, *f.domainMapperStateless, gravity_gradient_true, displacement_true, reference_action_true

View File

@@ -361,3 +361,80 @@ TEST_CASE(
CHECK(recovered->preparedResidual);
CHECK(preparedOperator.IsPrepared());
}
TEST_CASE(
"Hydrostatic Tensor Preparation Matches Pointwise Preparation And Rebuilds On Mode Changes",
tags::barotrope_hydrostatic_prepared_residual &tags::unit
) {
const auto args = test_utils::setup_args();
auto f = mean_field::fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.okay());
using namespace prepared_hydrostatic_test_utils;
mean_field::operators::PreparedHydrostaticEquilibriumOperator fast(f, *f.domainMapperStateless);
mean_field::operators::PreparedHydrostaticEquilibriumOperator reference(f, *f.domainMapperStateless);
auto h = make_enthalpy(f), phi = make_gravity_potential(f);
const auto dh = make_enthalpy(f, 0.53), dp = make_gravity_potential(f, 0.71);
const auto du = field_dof_test_utils::make_supported_displacement(f, 0.31);
const auto rotation = make_rotation();
auto dependencies = make_dependencies();
for (double scale : {0.0, 0.42, 0.73}) {
CAPTURE(scale);
const auto displacement = field_dof_test_utils::make_supported_displacement(f, scale);
h *= 1.01; phi *= 0.99;
++dependencies.displacement.revision;
++dependencies.enthalpy.revision;
++dependencies.gravityPotential.revision;
const auto state = make_state(h, phi, displacement, 0.39);
REQUIRE(fast.Prepare(state, dependencies, rotation).DidAnyWork());
REQUIRE(reference.PrepareReference(state, dependencies, rotation).DidAnyWork());
CHECK(fast.GetTensorDisplacementElementCount() > 0);
const auto compare = [&](auto &&apply) {
mfem::Vector a, b;
apply(fast, a); apply(reference, b);
CHECK(gravity_prepared_test_utils::relative_error(a, b, f.mesh->GetComm()) < 2.0e-11);
};
compare([](auto &op, auto &out) { op.BuildResidual(out); });
compare([&](auto &op, auto &out) { op.ApplyEnthalpyJacobianAction(dh, out); });
compare([&](auto &op, auto &out) { op.ApplyGravityPotentialJacobianAction(dp, out); });
compare([](auto &op, auto &out) { op.ApplyBernoulliConstantJacobianAction(0.13, out); });
compare([&](auto &op, auto &out) { op.ApplyDisplacementJacobianAction(du, out); });
compare([&](auto &op, auto &out) { op.ApplyCompleteJacobianAction(dh, dp, 0.13, du, out); });
compare([](auto &op, auto &out) { op.AssembleEnthalpyJacobianDiagonal(out); });
CHECK_FALSE(fast.Prepare(state, dependencies, rotation).DidAnyWork());
CHECK_FALSE(reference.PrepareReference(state, dependencies, rotation).DidAnyWork());
CHECK(fast.PrepareReference(state, dependencies, rotation).DidAnyWork());
compare([](auto &op, auto &out) { op.BuildResidual(out); });
CHECK_FALSE(fast.PrepareReference(state, dependencies, rotation).DidAnyWork());
CHECK(fast.Prepare(state, dependencies, rotation).DidAnyWork());
compare([&](auto &op, auto &out) { op.ApplyCompleteJacobianAction(dh, dp, 0.13, du, out); });
CHECK_FALSE(fast.Prepare(state, dependencies, rotation).DidAnyWork());
}
mfem::ParGridFunction foldedField(f.displacementFes.get());
mfem::VectorFunctionCoefficient fold(3, [](const mfem::Vector &x, mfem::Vector &u) {
u = x; u *= -2.0;
});
foldedField.ProjectCoefficient(fold);
mfem::Vector foldedTrue; foldedField.GetTrueDofs(foldedTrue);
const auto folded = fast.GetDisplacementMap().gather(foldedTrue);
++dependencies.displacement.revision;
const auto rejectedState = make_state(h, phi, folded, 0.39);
const auto rejected = fast.TryPrepare(rejectedState, dependencies, rotation);
const auto referenceRejected = reference.TryPrepareReference(rejectedState, dependencies, rotation);
REQUIRE_FALSE(rejected.has_value());
REQUIRE_FALSE(referenceRejected.has_value());
CHECK(rejected.error().reason == referenceRejected.error().reason);
CHECK(rejected.error().mappingStatus == mean_field::mapping::MappingStatus::non_positive_determinant);
CHECK_FALSE(fast.IsPrepared());
CHECK_FALSE(reference.IsPrepared());
const auto recovered = make_state(h, phi, du, 0.39);
++dependencies.displacement.revision;
REQUIRE(fast.TryPrepare(recovered, dependencies, rotation).has_value());
REQUIRE(reference.TryPrepareReference(recovered, dependencies, rotation).has_value());
mfem::Vector a, b;
fast.BuildResidual(a); reference.BuildResidual(b);
CHECK(gravity_prepared_test_utils::relative_error(a, b, f.mesh->GetComm()) < 2.0e-11);
}

View File

@@ -198,6 +198,16 @@ TEST_CASE(
CHECK(statistics.applications == 3);
CHECK(centeredDifferenceError < 2.0e-7);
CHECK(linearityError < 5.0e-12);
int stellarElements = 0;
preparedOperator.VisitMappedGeometryRules([&](int, const mfem::IntegrationRule &) { ++stellarElements; });
CHECK(preparedOperator.GetFastDisplacementElementCount() == stellarElements);
mfem::Vector preparedReference;
preparedOperator.ApplyDisplacementJacobianActionReference(firstVariation, preparedReference);
CHECK(prepared_hydrostatic_displacement_test_utils::relative_error(
firstAction, preparedReference, f.mesh->GetComm()) < 2.0e-11);
preparedOperator.ApplyDisplacementJacobianActionReference(secondVariation, preparedReference);
CHECK(prepared_hydrostatic_displacement_test_utils::relative_error(
secondAction, preparedReference, f.mesh->GetComm()) < 2.0e-11);
}
TEST_CASE(
@@ -344,4 +354,13 @@ TEST_CASE(
CHECK(displacementStatistics.applications == 4);
CHECK(geometryReferenceError < 2.0e-7);
CHECK(geometryEffect > 1.0e-8);
mfem::Vector preparedReference;
preparedOperator.ApplyDisplacementJacobianActionReference(displacementVariation, preparedReference);
CHECK(prepared_hydrostatic_displacement_test_utils::relative_error(
geometryUpdatedAction, preparedReference, f.mesh->GetComm()) < 2.0e-11);
preparedOperator.ApplyDisplacementJacobianActionCachedReference(displacementVariation, preparedReference);
CHECK(prepared_hydrostatic_displacement_test_utils::relative_error(
geometryUpdatedAction, preparedReference, f.mesh->GetComm()) < 2.0e-11);
CHECK(preparedOperator.GetTensorDisplacementElementCount() > 0);
CHECK(preparedOperator.GetDisplacementJacobianStatistics().applications == 4);
}

View File

@@ -439,10 +439,26 @@ TEST_CASE(
massOperator.Prepare({.targetMass = 1.0}, dependencies);
const std::uint64_t preparationCount = massOperator.GetPreparationCount();
CHECK(massOperator.GetJacobianRowPreparationCount() == 0);
const auto rhoDirection = gravityContext.GetDensityMap().gather(
mass_normalization_test_utils::make_density_direction(f, 0.67));
const auto uDirection = gravityContext.GetDisplacementMap().gather(
mass_normalization_test_utils::make_displacement_direction(f, -0.53));
auto checkRows = [&](std::uint64_t expectedBuilds) {
REQUIRE(massOperator.PrepareJacobianRows());
CHECK(massOperator.GetJacobianRowPreparationCount() == expectedBuilds);
mfem::Vector actual, reference;
massOperator.ApplyCompleteJacobianAction(rhoDirection, uDirection, actual);
massOperator.ApplyCompleteJacobianActionReference(rhoDirection, uDirection, reference);
CHECK(mass_normalization_test_utils::relative_error(actual(0), reference(0)) < 2.0e-11);
CHECK(massOperator.GetJacobianRowPreparationCount() == expectedBuilds);
};
checkRows(1);
const auto repeated = massOperator.Prepare({.targetMass = 1.0}, dependencies);
CHECK_FALSE(repeated.DidAnyWork());
CHECK(massOperator.GetPreparationCount() == preparationCount);
checkRows(1);
++gravityPotentialRevision;
mass_normalization_test_utils::prepare_gravity_context(
@@ -451,6 +467,7 @@ TEST_CASE(
const auto potentialOnly = massOperator.Prepare({.targetMass = 1.0}, dependencies);
CHECK_FALSE(potentialOnly.DidAnyWork());
checkRows(1);
const double residualBeforeTargetChange = mass_normalization_test_utils::residual_value(massOperator);
const double massBeforeTargetChange = massOperator.GetCurrentMass();
@@ -463,6 +480,7 @@ TEST_CASE(
CHECK_FALSE(targetOnly.refreshedGeometry);
CHECK_FALSE(targetOnly.refreshedDensity);
CHECK(massOperator.GetCurrentMass() == massBeforeTargetChange);
checkRows(1);
CHECK(
mass_normalization_test_utils::relative_error(
mass_normalization_test_utils::residual_value(massOperator) - residualBeforeTargetChange, -0.4
@@ -481,6 +499,8 @@ TEST_CASE(
CHECK(densityOnly.assembledResidual);
CHECK_FALSE(densityOnly.refreshedGeometry);
CHECK(massOperator.GetCurrentMass() != massBeforeDensityChange);
CHECK(massOperator.GetJacobianRowPreparationCount() == 1);
checkRows(2);
displacement = mass_normalization_test_utils::make_displacement_direction(f, 0.87);
++dependencies.displacement.revision;
@@ -492,6 +512,8 @@ TEST_CASE(
CHECK(geometryOnly.refreshedGeometry);
CHECK(geometryOnly.assembledResidual);
CHECK_FALSE(geometryOnly.refreshedDensity);
CHECK(massOperator.GetJacobianRowPreparationCount() == 2);
checkRows(3);
}
TEST_CASE(
@@ -587,6 +609,11 @@ TEST_CASE(
massOperator.ApplyDisplacementJacobianAction(reducedDisplacementDirection, displacementAction);
massOperator.ApplyCompleteJacobianAction(reducedDensityDirection, reducedDisplacementDirection, completeAction);
mfem::Vector preparedReference;
massOperator.ApplyCompleteJacobianActionReference(
reducedDensityDirection, reducedDisplacementDirection, preparedReference);
CHECK(mass_normalization_test_utils::relative_error(completeAction(0), preparedReference(0)) < 2.0e-11);
CHECK(
mass_normalization_test_utils::relative_error(completeAction(0), densityAction(0) + displacementAction(0)) <
2.0e-15

View File

@@ -439,6 +439,15 @@ TEST_CASE(
mfem::Vector kernelDisplacementActionTrue;
preparedOperator.ApplyDisplacementJacobianAction(displacementDirection, preparedDisplacementAction);
REQUIRE(preparedOperator.GetTensorElementCount() > 0);
mfem::Vector cachedDisplacementAction;
preparedOperator.ApplyDisplacementJacobianActionCachedReference(displacementDirection, cachedDisplacementAction);
CHECK(prepared_pressure_force_test_utils::relative_difference(
preparedDisplacementAction, cachedDisplacementAction, f.mesh->GetComm()) < 2.0e-12);
mfem::Vector retainedDisplacementAction;
preparedOperator.ApplyDisplacementJacobianActionReference(displacementDirection, retainedDisplacementAction);
CHECK(prepared_pressure_force_test_utils::relative_difference(
preparedDisplacementAction, retainedDisplacementAction, f.mesh->GetComm()) < 2.0e-12);
mean_field::operators::kernels::apply_pressure_force_displacement_action(
f, *f.domainMapperStateless, equationOfState, enthalpyTrue, displacementDirectionTrue, displacementTrue,
@@ -465,6 +474,30 @@ TEST_CASE(
prepared_pressure_force_test_utils::relative_difference(fusedAction, expectedFusedAction, f.mesh->GetComm()) <
2.0e-12
);
mfem::Vector originalResidual, referenceResidual, referenceEnthalpy;
preparedOperator.BuildResidual(originalResidual);
const auto state = mean_field::operators::context::pressure_force::PressureForceStateView{
.enthalpy = enthalpy, .displacement = displacement};
const auto dependencies = prepared_pressure_force_test_utils::make_dependencies();
CHECK(preparedOperator.PrepareReference(state, dependencies).DidAnyWork());
preparedOperator.BuildResidual(referenceResidual);
preparedOperator.ApplyEnthalpyJacobianAction(enthalpyDirection, referenceEnthalpy);
CHECK(prepared_pressure_force_test_utils::relative_difference(
originalResidual, referenceResidual, f.mesh->GetComm()) < 2.0e-12);
CHECK(prepared_pressure_force_test_utils::relative_difference(
preparedEnthalpyAction, referenceEnthalpy, f.mesh->GetComm()) < 2.0e-12);
CHECK_FALSE(preparedOperator.PrepareReference(state, dependencies).DidAnyWork());
CHECK(preparedOperator.PrepareCachedReference(state, dependencies).DidAnyWork());
preparedOperator.BuildResidual(referenceResidual);
preparedOperator.ApplyEnthalpyJacobianAction(enthalpyDirection, referenceEnthalpy);
CHECK(prepared_pressure_force_test_utils::relative_difference(
originalResidual, referenceResidual, f.mesh->GetComm()) < 2.0e-12);
CHECK(prepared_pressure_force_test_utils::relative_difference(
preparedEnthalpyAction, referenceEnthalpy, f.mesh->GetComm()) < 2.0e-12);
CHECK_FALSE(preparedOperator.PrepareCachedReference(state, dependencies).DidAnyWork());
CHECK(preparedOperator.Prepare(state, dependencies).DidAnyWork());
CHECK_FALSE(preparedOperator.Prepare(state, dependencies).DidAnyWork());
}
TEST_CASE(

View File

@@ -551,6 +551,19 @@ TEST_CASE(
CHECK(rotationReport.preparedResidual);
CHECK(preparedOperator.GetResidualPreparationCount() == 3);
CHECK(preparedOperator.GetResidualApplicationCount() == 1);
mfem::Vector updatedResidual, referenceResidual;
preparedOperator.BuildResidual(updatedResidual);
CHECK(preparedOperator.PrepareReference(
{.density = density, .displacement = displacement}, dependencies, rotation).DidAnyWork());
preparedOperator.BuildResidual(referenceResidual);
CHECK(rotational_displacement_force_test_utils::relative_difference(
updatedResidual, referenceResidual, f.mesh->GetComm()) < 2.0e-12);
CHECK_FALSE(preparedOperator.PrepareReference(
{.density = density, .displacement = displacement}, dependencies, rotation).DidAnyWork());
CHECK(preparedOperator.Prepare(
{.density = density, .displacement = displacement}, dependencies, rotation).DidAnyWork());
CHECK_FALSE(preparedOperator.Prepare(
{.density = density, .displacement = displacement}, dependencies, rotation).DidAnyWork());
}
TEST_CASE(
@@ -598,6 +611,16 @@ TEST_CASE(
preparedOperator.ApplyCompleteJacobianAction(densityDirection, displacementDirection, completeAction);
mfem::Vector retainedAction;
preparedOperator.ApplyCompleteJacobianActionReference(densityDirection, displacementDirection, retainedAction);
CHECK(preparedOperator.GetFastElementCount() > 0);
CHECK(preparedOperator.GetTensorElementCount() == preparedOperator.GetFastElementCount());
CHECK(rotational_displacement_force_test_utils::relative_difference(
completeAction, retainedAction, f.mesh->GetComm()) < 2.0e-12);
preparedOperator.ApplyCompleteJacobianActionCachedReference(densityDirection, displacementDirection, retainedAction);
CHECK(rotational_displacement_force_test_utils::relative_difference(
completeAction, retainedAction, f.mesh->GetComm()) < 2.0e-12);
mfem::Vector summedColumns(densityAction);
summedColumns += displacementAction;

View File

@@ -2,6 +2,7 @@
#include <array>
#include <cmath>
#include <limits>
#include <stdexcept>
#include <type_traits>
#include <catch2/catch_approx.hpp>
@@ -313,6 +314,8 @@ TEST_CASE(
preconditioning::GravityApproximateLDU{}
);
auto chebyshevPrepared = preconditioning::prepare(geometry.finiteElements, geometry.context, chebyshevBlock);
CHECK(prepared.GetDivergenceAssemblyCount() == 1);
CHECK(chebyshevPrepared.GetDivergenceAssemblyCount() == 1);
const auto unchanged = prepared.Refresh(geometry.finiteElements, geometry.context);
const auto unchangedChebyshev = chebyshevPrepared.Refresh(geometry.finiteElements, geometry.context);
@@ -338,6 +341,8 @@ TEST_CASE(
const std::uint64_t sourcePreparations = geometry.context.GetSourceOperator().GetPreparationCount();
const auto changed = prepared.Refresh(geometry.finiteElements, geometry.context);
const auto changedChebyshev = chebyshevPrepared.Refresh(geometry.finiteElements, geometry.context);
CHECK(prepared.GetDivergenceAssemblyCount() == 1);
CHECK(chebyshevPrepared.GetDivergenceAssemblyCount() == 1);
CHECK(changed.geometryChanged);
CHECK_FALSE(changed.discretizationChanged);
@@ -377,6 +382,7 @@ TEST_CASE(
CHECK(reconstructed.rebuiltPotentialSchur);
CHECK(chebyshevPrepared.IsCurrent());
CHECK(chebyshevPrepared.GetMassInverse().GetStatistics().setups == 3);
CHECK(chebyshevPrepared.GetDivergenceAssemblyCount() == 2);
mfem::Vector firstReconstructedAction(chebyshevPrepared.Height());
mfem::Vector secondReconstructedAction(chebyshevPrepared.Height());
@@ -391,3 +397,52 @@ TEST_CASE(
CHECK(std::isfinite(firstReconstructedAction(index)));
}
}
TEST_CASE(
"Fixed Gravity Divergence Cache Preserves Changing Mass Weights And Invalidates",
tags::preconditioning_gravity_integration
) {
const auto args = test_utils::setup_args();
auto f = mean_field::fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.okay());
preconditioning::PreparedGravityDivergenceCache cache;
mfem::Vector diagonal(f.gravityFluxFes->GetTrueVSize()); diagonal = 1.0;
const auto x = gravity_prepared_test_utils::make_deterministic_vector(f.gravityPotentialFes->GetTrueVSize(), 0.37);
auto initial = preconditioning::assembleGravityPotentialSchurSurrogate(f, diagonal, &cache);
mfem::Vector initialAction(initial->Height()); initial->Mult(x, initialAction);
REQUIRE(initialAction.Norml2() > 0.0);
REQUIRE(cache.GetBuildCount() == 1);
for (double scale : {2.0, 0.5, 1.0}) {
CAPTURE(scale);
diagonal = scale;
auto fast = preconditioning::assembleGravityPotentialSchurSurrogate(f, diagonal, &cache);
auto reference = preconditioning::assembleGravityPotentialSchurSurrogate(f, diagonal);
mfem::Vector a(fast->Height()), b(reference->Height()), expected(initialAction);
fast->Mult(x, a); reference->Mult(x, b); expected /= scale;
CHECK(gravity_prepared_test_utils::relative_error(a, b, f.mesh->GetComm()) < 2.0e-12);
CHECK(gravity_prepared_test_utils::relative_error(a, expected, f.mesh->GetComm()) < 2.0e-12);
CHECK(cache.GetBuildCount() == 1);
initial->Mult(x, a);
CHECK(gravity_prepared_test_utils::relative_error(a, initialAction, f.mesh->GetComm()) < 2.0e-14);
}
for (double bad : {0.0, -1.0, std::numeric_limits<double>::quiet_NaN()}) {
diagonal = bad;
CHECK_THROWS_AS(
preconditioning::assembleGravityPotentialSchurSurrogate(f, diagonal, &cache), std::invalid_argument);
CHECK(cache.GetBuildCount() == 1);
}
diagonal = 1.0;
const auto checkRebuild = [&] {
auto fast = preconditioning::assembleGravityPotentialSchurSurrogate(f, diagonal, &cache);
auto reference = preconditioning::assembleGravityPotentialSchurSurrogate(f, diagonal);
mfem::Vector a(fast->Height()), b(reference->Height());
fast->Mult(x, a); reference->Mult(x, b);
CHECK(gravity_prepared_test_utils::relative_error(a, b, f.mesh->GetComm()) < 2.0e-12);
};
cache.Clear(); checkRebuild();
CHECK(cache.GetBuildCount() == 2);
f.mesh->NodesUpdated(); checkRebuild();
CHECK(cache.GetBuildCount() == 3);
checkRebuild();
CHECK(cache.GetBuildCount() == 3);
}

View File

@@ -1552,3 +1552,48 @@ TEST_CASE(
CHECK(report.lastAcceptedCheckpointView().valid());
}
}
TEST_CASE("Skipping Geometry Preflight Retains Trial Acceptance And Fold Rejection",
"[solver][newton][geometry][preflight][backtracking][wiring]") {
using namespace mean_field;
using namespace stellar_solver_architecture_test;
for (bool folding : {false, true}) {
CAPTURE(folding);
auto finiteElements = makeFiniteElements();
auto discretization = equilibrium::makeStellarDiscretization(
std::move(finiteElements),
normalization::PhysicalRieszDiagonal{dimensions::LengthValue{utils::RADIUS}, utils::G});
auto context = solver::makeContext(
makeModel(), std::move(discretization), preconditioning::makePreconditioner(),
ScriptedBackend{nullptr, folding ? -10.0 : 0.0, false, true});
auto metricState = std::make_shared<MetricSequenceState>();
metricState->evaluations = {{.residualNorm = 2.0, .merit = 2.0}, {.residualNorm = 1.0, .merit = 0.5}};
bool observed = false;
auto observer = solver::nonlinear::makeObserver(
[](const solver::nonlinear::BeforeIteration &) {},
[&](const solver::nonlinear::AfterIteration &event) {
observed = true;
CHECK_FALSE(event.geometryPreflight.has_value());
CHECK(event.geometryPreflightSeconds == 0.0);
CHECK(event.lineSearchTrials == 1);
CHECK(event.stepAccepted == !folding);
});
auto newton = solver::nonlinear::Newton(
solver::nonlinear::NewtonOptions{
.relativeTolerance = 0.0, .absoluteTolerance = 0.0, .maximumIterations = 1,
.linearSolve = {.relativeTolerance = 0.0, .absoluteTolerance = std::numeric_limits<double>::max(),
.maximumIterations = 1},
.backtracking = {.maximumTrials = 1, .geometryPreflight = false}},
SequencedMetric{metricState});
auto solverInstance = solver::make(context, std::move(newton), std::move(observer));
const auto report = solverInstance.evaluate();
CHECK(observed);
CHECK_FALSE(report.diagnostics().lastGeometryPreflight.has_value());
CHECK(report.diagnostics().totalGeometryPreflightSeconds == 0.0);
CHECK(report.diagnostics().geometryLimitedIterations == 0);
CHECK(report.diagnostics().totalLineSearchTrials == 1);
CHECK(report.diagnostics().acceptedNonlinearIterations == (folding ? 0 : 1));
CHECK(report.diagnostics().inadmissibleLineSearchTrials == (folding ? 1 : 0));
CHECK(report.lastAcceptedCheckpointView().valid());
}
}