feat(libmeanfield): centrifugal + pressure

This commit is contained in:
2026-08-04 14:24:55 -04:00
parent 9bc4f2758a
commit dc912fd15e
115 changed files with 260058 additions and 163261 deletions

View File

@@ -1,25 +1,27 @@
#include <algorithm>
#include <catch2/benchmark/catch_benchmark.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <catch2/benchmark/catch_benchmark.hpp>
#include <cmath>
#include <vector>
#include <string>
#include <algorithm>
#include <mfem.hpp>
#include <mpi.h>
#include <string>
#include <vector>
import mean_field;
import test_helpers;
using namespace mean_field;
TEST_CASE("STROID Volume vs sphere", tags::geometry & tags::volume) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
TEST_CASE(
"STROID Volume vs sphere",
tags::geometry &tags::volume
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
const double analytic_vol = (4.0 / 3.0) * M_PI * std::pow(utils::RADIUS, 3);
const double stroid_vol = analysis::get_mesh_volume(f);
const double stroid_vol = analysis::get_mesh_volume(f);
double s = analytic_vol / stroid_vol;
double s = analytic_vol / stroid_vol;
CHECK_THAT(stroid_vol, Catch::Matchers::WithinRel(analytic_vol, 1e-6));
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,931 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <mfem.hpp>
import mean_field;
import test_helpers;
using namespace mean_field;
TEST_CASE(
"Gravity Force Integrator Jacobian Matches Residual Linearization",
tags::unit &tags::solver &tags::integrator &tags::gravity
) {
constexpr int dim = 3;
constexpr double finite_difference_step = 1.0e-3;
constexpr double jacobian_tolerance = 1.0e-8;
constexpr double zero_tolerance = 1.0e-14;
constexpr int velocity_block =
solver::block_index(solver::FieldBlock::velocity);
constexpr int density_block =
solver::block_index(solver::FieldBlock::density);
constexpr int gravity_gradient_block =
solver::block_index(solver::FieldBlock::gravity_gradient);
constexpr int gravity_potential_block =
solver::block_index(solver::FieldBlock::gravity_potential);
constexpr int displacement_block =
solver::block_index(solver::FieldBlock::displacement);
constexpr int block_count = solver::field_block_count;
mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D(
1, 1, 1, mfem::Element::HEXAHEDRON, 1.0, 1.0, 1.0
);
mfem::H1_FECollection velocity_fec(2, dim);
mfem::L2_FECollection density_fec(1, dim);
mfem::RT_FECollection gravity_gradient_fec(1, dim);
mfem::L2_FECollection gravity_potential_fec(1, dim);
mfem::H1_FECollection displacement_fec(2, dim);
mfem::FiniteElementSpace velocity_fes(
&mesh, &velocity_fec, dim, mfem::Ordering::byVDIM
);
mfem::FiniteElementSpace density_fes(&mesh, &density_fec);
mfem::FiniteElementSpace gravity_gradient_fes(&mesh, &gravity_gradient_fec);
mfem::FiniteElementSpace gravity_potential_fes(
&mesh, &gravity_potential_fec
);
mfem::FiniteElementSpace displacement_fes(
&mesh, &displacement_fec, dim, mfem::Ordering::byVDIM
);
mfem::GridFunction displacement(&displacement_fes);
displacement = 0.0;
mapping::DomainMapper domain_mapper(displacement, 1.0, 2.0);
INFO(
std::format(
"Domain mapping is has displacement field: {}",
domain_mapper.HasDisplacementField()
)
);
INFO(
std::format(
"Domain mapping is identity: {}", domain_mapper.CalcIsIdentity()
)
);
REQUIRE(domain_mapper.CalcIsIdentity());
const mfem::FiniteElement *velocity_element = velocity_fes.GetFE(0);
const mfem::FiniteElement *density_element = density_fes.GetFE(0);
const mfem::FiniteElement *gravity_gradient_element =
gravity_gradient_fes.GetFE(0);
const mfem::FiniteElement *gravity_potential_element =
gravity_potential_fes.GetFE(0);
const mfem::FiniteElement *displacement_element = displacement_fes.GetFE(0);
mfem::ElementTransformation *transformation =
mesh.GetElementTransformation(0);
const int velocity_dofs_count = velocity_element->GetDof();
const int density_dofs_count = density_element->GetDof();
const int gravity_gradient_dofs_count = gravity_gradient_element->GetDof();
const int gravity_potential_dofs_count =
gravity_potential_element->GetDof();
const int displacement_dofs_count = displacement_element->GetDof();
const int velocity_size = dim * velocity_dofs_count;
const int displacement_size = dim * displacement_dofs_count;
mfem::Vector velocity_dofs(velocity_size);
mfem::Vector density_dofs(density_dofs_count);
mfem::Vector gravity_gradient_dofs(gravity_gradient_dofs_count);
mfem::Vector gravity_potential_dofs(gravity_potential_dofs_count);
mfem::Vector displacement_dofs(displacement_size);
velocity_dofs = 0.0;
gravity_potential_dofs = 0.0;
displacement_dofs = 0.0;
for (int i = 0; i < density_dofs_count; ++i) {
density_dofs(i) = 0.8 + 0.07 * static_cast<double>(i + 1);
}
for (int i = 0; i < gravity_gradient_dofs_count; ++i) {
const double sign = i % 2 == 0 ? 1.0 : -1.0;
gravity_gradient_dofs(i) = sign * 0.04 * static_cast<double>(i + 1);
}
mfem::Vector density_direction(density_dofs_count);
mfem::Vector gravity_gradient_direction(gravity_gradient_dofs_count);
mfem::Vector velocity_direction(velocity_size);
mfem::Vector displacement_direction(displacement_size);
for (int i = 0; i < density_dofs_count; ++i) {
density_direction(i) = 0.13 - 0.02 * static_cast<double>(i);
}
for (int i = 0; i < gravity_gradient_dofs_count; ++i) {
const double sign = i % 3 == 0 ? -1.0 : 1.0;
gravity_gradient_direction(i) =
sign * (0.03 + 0.005 * static_cast<double>(i));
}
for (int i = 0; i < velocity_size; ++i) {
velocity_direction(i) = 0.01 * static_cast<double>(i + 1);
}
for (int i = 0; i < displacement_size; ++i) {
displacement_direction(i) = -0.008 * static_cast<double>(i + 1);
}
mfem::Array<const mfem::FiniteElement *> elements(block_count);
elements[velocity_block] = velocity_element;
elements[density_block] = density_element;
elements[gravity_gradient_block] = gravity_gradient_element;
elements[gravity_potential_block] = gravity_potential_element;
elements[displacement_block] = displacement_element;
mfem::Array<const mfem::Vector *> element_state(block_count);
element_state[velocity_block] = &velocity_dofs;
element_state[density_block] = &density_dofs;
element_state[gravity_gradient_block] = &gravity_gradient_dofs;
element_state[gravity_potential_block] = &gravity_potential_dofs;
element_state[displacement_block] = &displacement_dofs;
mfem::Vector velocity_residual(velocity_size);
mfem::Vector density_residual(density_dofs_count);
mfem::Vector gravity_gradient_residual(gravity_gradient_dofs_count);
mfem::Vector gravity_potential_residual(gravity_potential_dofs_count);
mfem::Vector displacement_residual(displacement_size);
mfem::Array<mfem::Vector *> element_residual(block_count);
element_residual[velocity_block] = &velocity_residual;
element_residual[density_block] = &density_residual;
element_residual[gravity_gradient_block] = &gravity_gradient_residual;
element_residual[gravity_potential_block] = &gravity_potential_residual;
element_residual[displacement_block] = &displacement_residual;
integrators::GravityMomentumIntegrator integrator(
domain_mapper, integrators::GravityForceJacobianMode::field_coupled
);
const int maximum_order = std::max(
velocity_element->GetOrder(),
std::max(
density_element->GetOrder(), gravity_gradient_element->GetOrder()
)
);
const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get(
velocity_element->GetGeomType(), 2 * maximum_order + 8
);
integrator.SetIntegrationRule(integration_rule);
mfem::DenseMatrix dv_dv(velocity_size, velocity_size);
mfem::DenseMatrix dv_drho(velocity_size, density_dofs_count);
mfem::DenseMatrix dv_dgrad_phi(velocity_size, gravity_gradient_dofs_count);
mfem::DenseMatrix dv_ddisplacement(velocity_size, displacement_size);
dv_dv = 1.0;
dv_drho = 1.0;
dv_dgrad_phi = 1.0;
dv_ddisplacement = 1.0;
mfem::Array2D<mfem::DenseMatrix *> element_matrices(
block_count, block_count
);
for (int row = 0; row < block_count; ++row) {
for (int column = 0; column < block_count; ++column) {
element_matrices(row, column) = nullptr;
}
}
element_matrices(velocity_block, velocity_block) = &dv_dv;
element_matrices(velocity_block, density_block) = &dv_drho;
element_matrices(velocity_block, gravity_gradient_block) = &dv_dgrad_phi;
element_matrices(velocity_block, displacement_block) = &dv_ddisplacement;
integrator.AssembleElementGrad(
elements, *transformation, element_state, element_matrices
);
auto assemble_velocity_residual =
[&](const mfem::Vector &density_state,
const mfem::Vector &gravity_gradient_state) {
element_state[density_block] = &density_state;
element_state[gravity_gradient_block] = &gravity_gradient_state;
integrator.AssembleElementVector(
elements, *transformation, element_state, element_residual
);
return mfem::Vector(velocity_residual);
};
auto relative_error = [](mfem::Vector computed,
const mfem::Vector &reference) {
computed -= reference;
return computed.Norml2() / std::max(reference.Norml2(), 1.0e-30);
};
mfem::Vector density_plus(density_dofs);
mfem::Vector density_minus(density_dofs);
density_plus.Add(finite_difference_step, density_direction);
density_minus.Add(-finite_difference_step, density_direction);
mfem::Vector density_residual_plus =
assemble_velocity_residual(density_plus, gravity_gradient_dofs);
mfem::Vector density_residual_minus =
assemble_velocity_residual(density_minus, gravity_gradient_dofs);
mfem::Vector density_finite_difference(density_residual_plus);
density_finite_difference -= density_residual_minus;
density_finite_difference *= 0.5 / finite_difference_step;
mfem::Vector density_jacobian_action(velocity_size);
dv_drho.Mult(density_direction, density_jacobian_action);
mfem::Vector gravity_gradient_plus(gravity_gradient_dofs);
mfem::Vector gravity_gradient_minus(gravity_gradient_dofs);
gravity_gradient_plus.Add(
finite_difference_step, gravity_gradient_direction
);
gravity_gradient_minus.Add(
-finite_difference_step, gravity_gradient_direction
);
mfem::Vector gravity_residual_plus =
assemble_velocity_residual(density_dofs, gravity_gradient_plus);
mfem::Vector gravity_residual_minus =
assemble_velocity_residual(density_dofs, gravity_gradient_minus);
mfem::Vector gravity_finite_difference(gravity_residual_plus);
gravity_finite_difference -= gravity_residual_minus;
gravity_finite_difference *= 0.5 / finite_difference_step;
mfem::Vector gravity_jacobian_action(velocity_size);
dv_dgrad_phi.Mult(gravity_gradient_direction, gravity_jacobian_action);
mfem::Vector combined_density_plus(density_dofs);
mfem::Vector combined_density_minus(density_dofs);
mfem::Vector combined_gravity_plus(gravity_gradient_dofs);
mfem::Vector combined_gravity_minus(gravity_gradient_dofs);
combined_density_plus.Add(finite_difference_step, density_direction);
combined_density_minus.Add(-finite_difference_step, density_direction);
combined_gravity_plus.Add(
finite_difference_step, gravity_gradient_direction
);
combined_gravity_minus.Add(
-finite_difference_step, gravity_gradient_direction
);
mfem::Vector combined_residual_plus = assemble_velocity_residual(
combined_density_plus, combined_gravity_plus
);
mfem::Vector combined_residual_minus = assemble_velocity_residual(
combined_density_minus, combined_gravity_minus
);
mfem::Vector combined_finite_difference(combined_residual_plus);
combined_finite_difference -= combined_residual_minus;
combined_finite_difference *= 0.5 / finite_difference_step;
mfem::Vector combined_jacobian_action(density_jacobian_action);
combined_jacobian_action += gravity_jacobian_action;
mfem::Vector inactive_velocity_action(velocity_size);
mfem::Vector inactive_displacement_action(velocity_size);
dv_dv.Mult(velocity_direction, inactive_velocity_action);
dv_ddisplacement.Mult(displacement_direction, inactive_displacement_action);
const double density_relative_error =
relative_error(density_finite_difference, density_jacobian_action);
const double gravity_relative_error =
relative_error(gravity_finite_difference, gravity_jacobian_action);
const double combined_relative_error =
relative_error(combined_finite_difference, combined_jacobian_action);
INFO("Density Jacobian relative error = " << density_relative_error);
INFO(
"Gravity-gradient Jacobian relative error = " << gravity_relative_error
);
INFO("Combined Jacobian relative error = " << combined_relative_error);
CHECK_THAT(
density_relative_error,
Catch::Matchers::WithinAbs(0.0, jacobian_tolerance)
);
CHECK_THAT(
gravity_relative_error,
Catch::Matchers::WithinAbs(0.0, jacobian_tolerance)
);
CHECK_THAT(
combined_relative_error,
Catch::Matchers::WithinAbs(0.0, jacobian_tolerance)
);
CHECK_THAT(
inactive_velocity_action.Norml2(),
Catch::Matchers::WithinAbs(0.0, zero_tolerance)
);
CHECK_THAT(
inactive_displacement_action.Norml2(),
Catch::Matchers::WithinAbs(0.0, zero_tolerance)
);
mfem::Vector field_coupled_density_action(density_jacobian_action);
integrator.SetJacobianMode(integrators::GravityForceJacobianMode::minimal);
dv_dv = 1.0;
dv_drho = 1.0;
dv_dgrad_phi = 1.0;
dv_ddisplacement = 1.0;
element_state[density_block] = &density_dofs;
element_state[gravity_gradient_block] = &gravity_gradient_dofs;
integrator.AssembleElementGrad(
elements, *transformation, element_state, element_matrices
);
mfem::Vector minimal_density_action(velocity_size);
mfem::Vector minimal_gravity_action(velocity_size);
dv_drho.Mult(density_direction, minimal_density_action);
dv_dgrad_phi.Mult(gravity_gradient_direction, minimal_gravity_action);
const double minimal_density_difference =
relative_error(minimal_density_action, field_coupled_density_action);
INFO(
"Minimal-mode density-block difference = " << minimal_density_difference
);
CHECK_THAT(
minimal_density_difference,
Catch::Matchers::WithinAbs(0.0, zero_tolerance)
);
CHECK_THAT(
minimal_gravity_action.Norml2(),
Catch::Matchers::WithinAbs(0.0, zero_tolerance)
);
}
TEST_CASE(
"Gravity Force Integrator Matches Manufactured Cartesian Load",
tags::unit &tags::solver &tags::integrator &tags::gravity
) {
constexpr int dim = 3;
constexpr double tolerance = 1.0e-12;
constexpr int velocity_block =
solver::block_index(solver::FieldBlock::velocity);
constexpr int density_block =
solver::block_index(solver::FieldBlock::density);
constexpr int gravity_gradient_block =
solver::block_index(solver::FieldBlock::gravity_gradient);
constexpr int gravity_potential_block =
solver::block_index(solver::FieldBlock::gravity_potential);
constexpr int displacement_block =
solver::block_index(solver::FieldBlock::displacement);
constexpr int block_count = solver::field_block_count;
mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D(
1, 1, 1, mfem::Element::HEXAHEDRON, 1.0, 1.0, 1.0
);
mfem::H1_FECollection velocity_fec(1, dim);
mfem::L2_FECollection density_fec(1, dim);
mfem::RT_FECollection gravity_gradient_fec(0, dim);
mfem::H1_FECollection displacement_fec(1, dim);
mfem::FiniteElementSpace velocity_fes(
&mesh, &velocity_fec, dim, mfem::Ordering::byVDIM
);
mfem::FiniteElementSpace density_fes(&mesh, &density_fec);
mfem::FiniteElementSpace gravity_gradient_fes(&mesh, &gravity_gradient_fec);
mfem::FiniteElementSpace displacement_fes(
&mesh, &displacement_fec, dim, mfem::Ordering::byVDIM
);
mfem::GridFunction displacement(&displacement_fes);
displacement = 0.0;
mapping::DomainMapper domain_mapper(displacement, 1.0, 2.0);
REQUIRE(domain_mapper.CalcIsIdentity());
auto reference_density = [](const mfem::Vector &x) { return 1.0 + x(0); };
auto reference_gravity_gradient = [](const mfem::Vector &x,
mfem::Vector &gradient) {
gradient.SetSize(3);
gradient(0) = 2.0 * x(0);
gradient(1) = 3.0 * x(1);
gradient(2) = 4.0 * x(2);
};
mfem::FunctionCoefficient density_coefficient(reference_density);
mfem::VectorFunctionCoefficient gravity_gradient_coefficient(
dim, reference_gravity_gradient
);
mfem::GridFunction density(&density_fes);
mfem::GridFunction gravity_gradient(&gravity_gradient_fes);
density.ProjectCoefficient(density_coefficient);
gravity_gradient.ProjectCoefficient(gravity_gradient_coefficient);
const mfem::FiniteElement *velocity_element = velocity_fes.GetFE(0);
const mfem::FiniteElement *density_element = density_fes.GetFE(0);
const mfem::FiniteElement *gravity_gradient_element =
gravity_gradient_fes.GetFE(0);
const mfem::FiniteElement *displacement_element = displacement_fes.GetFE(0);
mfem::ElementTransformation *transformation =
mesh.GetElementTransformation(0);
const int velocity_dofs_count = velocity_element->GetDof();
const int density_dofs_count = density_element->GetDof();
const int gravity_gradient_dofs_count = gravity_gradient_element->GetDof();
const int displacement_dofs_count = displacement_element->GetDof();
const int velocity_size = dim * velocity_dofs_count;
const int displacement_size = dim * displacement_dofs_count;
mfem::Array<int> density_dof_indices;
mfem::Array<int> gravity_gradient_dof_indices;
mfem::Vector density_dofs;
mfem::Vector gravity_gradient_dofs;
density_fes.GetElementDofs(0, density_dof_indices);
gravity_gradient_fes.GetElementVDofs(0, gravity_gradient_dof_indices);
density.GetSubVector(density_dof_indices, density_dofs);
gravity_gradient.GetSubVector(
gravity_gradient_dof_indices, gravity_gradient_dofs
);
REQUIRE(density_dofs.Size() == density_dofs_count);
REQUIRE(gravity_gradient_dofs.Size() == gravity_gradient_dofs_count);
mfem::Vector velocity_dofs(velocity_size);
mfem::Vector gravity_potential_dofs(density_dofs_count);
mfem::Vector displacement_dofs(displacement_size);
velocity_dofs = 0.0;
gravity_potential_dofs = 0.0;
displacement_dofs = 0.0;
mfem::Array<const mfem::FiniteElement *> elements(block_count);
elements[velocity_block] = velocity_element;
elements[density_block] = density_element;
elements[gravity_gradient_block] = gravity_gradient_element;
elements[gravity_potential_block] = density_element;
elements[displacement_block] = displacement_element;
mfem::Array<const mfem::Vector *> element_state(block_count);
element_state[velocity_block] = &velocity_dofs;
element_state[density_block] = &density_dofs;
element_state[gravity_gradient_block] = &gravity_gradient_dofs;
element_state[gravity_potential_block] = &gravity_potential_dofs;
element_state[displacement_block] = &displacement_dofs;
mfem::Vector velocity_residual(velocity_size);
mfem::Vector density_residual(density_dofs_count);
mfem::Vector gravity_gradient_residual(gravity_gradient_dofs_count);
mfem::Vector gravity_potential_residual(density_dofs_count);
mfem::Vector displacement_residual(displacement_size);
mfem::Array<mfem::Vector *> element_residual(block_count);
element_residual[velocity_block] = &velocity_residual;
element_residual[density_block] = &density_residual;
element_residual[gravity_gradient_block] = &gravity_gradient_residual;
element_residual[gravity_potential_block] = &gravity_potential_residual;
element_residual[displacement_block] = &displacement_residual;
integrators::GravityMomentumIntegrator integrator(
domain_mapper, integrators::GravityForceJacobianMode::field_coupled
);
const mfem::IntegrationRule &integration_rule =
mfem::IntRules.Get(velocity_element->GetGeomType(), 8);
integrator.SetIntegrationRule(integration_rule);
integrator.AssembleElementVector(
elements, *transformation, element_state, element_residual
);
mfem::Vector reference_velocity_residual(velocity_residual);
auto residual_action = [&](const int component,
const int coordinate_weight) {
mfem::Vector test_dofs(velocity_size);
mfem::Vector x_physical(dim);
test_dofs = 0.0;
const mfem::IntegrationRule &velocity_nodes =
velocity_element->GetNodes();
for (int i = 0; i < velocity_dofs_count; ++i) {
const mfem::IntegrationPoint &node = velocity_nodes.IntPoint(i);
transformation->SetIntPoint(&node);
domain_mapper.GetPhysicalPoint(*transformation, node, x_physical);
test_dofs(i + component * velocity_dofs_count) =
coordinate_weight < 0 ? 1.0 : x_physical(coordinate_weight);
}
return test_dofs * velocity_residual;
};
CHECK_THAT(
residual_action(0, -1), Catch::Matchers::WithinAbs(5.0 / 3.0, tolerance)
);
CHECK_THAT(
residual_action(1, -1), Catch::Matchers::WithinAbs(9.0 / 4.0, tolerance)
);
CHECK_THAT(
residual_action(2, -1), Catch::Matchers::WithinAbs(3.0, tolerance)
);
CHECK_THAT(
residual_action(0, 0), Catch::Matchers::WithinAbs(7.0 / 6.0, tolerance)
);
CHECK_THAT(
residual_action(1, 1), Catch::Matchers::WithinAbs(3.0 / 2.0, tolerance)
);
CHECK_THAT(
residual_action(2, 2), Catch::Matchers::WithinAbs(2.0, tolerance)
);
CHECK_THAT(
residual_action(1, 0), Catch::Matchers::WithinAbs(5.0 / 4.0, tolerance)
);
CHECK_THAT(
density_residual.Norml2(), Catch::Matchers::WithinAbs(0.0, tolerance)
);
CHECK_THAT(
gravity_gradient_residual.Norml2(),
Catch::Matchers::WithinAbs(0.0, tolerance)
);
CHECK_THAT(
gravity_potential_residual.Norml2(),
Catch::Matchers::WithinAbs(0.0, tolerance)
);
CHECK_THAT(
displacement_residual.Norml2(),
Catch::Matchers::WithinAbs(0.0, tolerance)
);
integrator.SetJacobianMode(integrators::GravityForceJacobianMode::minimal);
integrator.AssembleElementVector(
elements, *transformation, element_state, element_residual
);
mfem::Vector minimal_difference(velocity_residual);
minimal_difference -= reference_velocity_residual;
integrator.SetJacobianMode(integrators::GravityForceJacobianMode::exact);
integrator.AssembleElementVector(
elements, *transformation, element_state, element_residual
);
mfem::Vector exact_difference(velocity_residual);
exact_difference -= reference_velocity_residual;
CHECK_THAT(
minimal_difference.Norml2(), Catch::Matchers::WithinAbs(0.0, tolerance)
);
CHECK_THAT(
exact_difference.Norml2(), Catch::Matchers::WithinAbs(0.0, tolerance)
);
}
TEST_CASE(
"Gravity Force Integrator Preserves Gravity Identities",
tags::unit &tags::solver &tags::integrator &tags::gravity
) {
constexpr int dim = 3;
constexpr double density_value = 1.7;
constexpr double gravity_scale = 2.4;
constexpr double density_scale = 0.6;
constexpr double tolerance = 1.0e-12;
constexpr int velocity_block =
solver::block_index(solver::FieldBlock::velocity);
constexpr int density_block =
solver::block_index(solver::FieldBlock::density);
constexpr int gravity_gradient_block =
solver::block_index(solver::FieldBlock::gravity_gradient);
constexpr int gravity_potential_block =
solver::block_index(solver::FieldBlock::gravity_potential);
constexpr int displacement_block =
solver::block_index(solver::FieldBlock::displacement);
constexpr int block_count = solver::field_block_count;
mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D(
1, 1, 1, mfem::Element::HEXAHEDRON, 1.0, 1.0, 1.0
);
mfem::H1_FECollection velocity_fec(1, dim);
mfem::L2_FECollection density_fec(0, dim);
mfem::RT_FECollection gravity_gradient_fec(0, dim);
mfem::H1_FECollection displacement_fec(1, dim);
mfem::FiniteElementSpace velocity_fes(
&mesh, &velocity_fec, dim, mfem::Ordering::byVDIM
);
mfem::FiniteElementSpace density_fes(&mesh, &density_fec);
mfem::FiniteElementSpace gravity_gradient_fes(&mesh, &gravity_gradient_fec);
mfem::FiniteElementSpace displacement_fes(
&mesh, &displacement_fec, dim, mfem::Ordering::byVDIM
);
mfem::GridFunction displacement(&displacement_fes);
displacement = 0.0;
mapping::DomainMapper domain_mapper(displacement, 1.0, 2.0);
REQUIRE(domain_mapper.HasDisplacementField());
auto radial_gravity = [](const mfem::Vector &x, mfem::Vector &gravity) {
gravity.SetSize(3);
gravity(0) = x(0) - 0.5;
gravity(1) = x(1) - 0.5;
gravity(2) = x(2) - 0.5;
};
mfem::ConstantCoefficient density_coefficient(density_value);
mfem::VectorFunctionCoefficient gravity_coefficient(dim, radial_gravity);
mfem::GridFunction density(&density_fes);
mfem::GridFunction gravity_gradient(&gravity_gradient_fes);
density.ProjectCoefficient(density_coefficient);
gravity_gradient.ProjectCoefficient(gravity_coefficient);
const mfem::FiniteElement *velocity_element = velocity_fes.GetFE(0);
const mfem::FiniteElement *density_element = density_fes.GetFE(0);
const mfem::FiniteElement *gravity_gradient_element =
gravity_gradient_fes.GetFE(0);
const mfem::FiniteElement *displacement_element = displacement_fes.GetFE(0);
mfem::ElementTransformation *transformation =
mesh.GetElementTransformation(0);
const int velocity_dofs_count = velocity_element->GetDof();
const int density_dofs_count = density_element->GetDof();
const int gravity_gradient_dofs_count = gravity_gradient_element->GetDof();
const int displacement_dofs_count = displacement_element->GetDof();
const int velocity_size = dim * velocity_dofs_count;
const int displacement_size = dim * displacement_dofs_count;
mfem::Array<int> density_dof_indices;
mfem::Array<int> gravity_gradient_dof_indices;
mfem::Vector density_dofs;
mfem::Vector gravity_gradient_dofs;
density_fes.GetElementDofs(0, density_dof_indices);
gravity_gradient_fes.GetElementVDofs(0, gravity_gradient_dof_indices);
density.GetSubVector(density_dof_indices, density_dofs);
gravity_gradient.GetSubVector(
gravity_gradient_dof_indices, gravity_gradient_dofs
);
mfem::Vector zero_density(density_dofs_count);
mfem::Vector zero_gravity(gravity_gradient_dofs_count);
mfem::Vector velocity_dofs(velocity_size);
mfem::Vector gravity_potential_dofs(density_dofs_count);
mfem::Vector displacement_dofs(displacement_size);
zero_density = 0.0;
zero_gravity = 0.0;
velocity_dofs = 0.0;
gravity_potential_dofs = 0.0;
displacement_dofs = 0.0;
mfem::Array<const mfem::FiniteElement *> elements(block_count);
elements[velocity_block] = velocity_element;
elements[density_block] = density_element;
elements[gravity_gradient_block] = gravity_gradient_element;
elements[gravity_potential_block] = density_element;
elements[displacement_block] = displacement_element;
mfem::Array<const mfem::Vector *> element_state(block_count);
element_state[velocity_block] = &velocity_dofs;
element_state[density_block] = &density_dofs;
element_state[gravity_gradient_block] = &gravity_gradient_dofs;
element_state[gravity_potential_block] = &gravity_potential_dofs;
element_state[displacement_block] = &displacement_dofs;
mfem::Vector velocity_residual(velocity_size);
mfem::Vector density_residual(density_dofs_count);
mfem::Vector gravity_gradient_residual(gravity_gradient_dofs_count);
mfem::Vector gravity_potential_residual(density_dofs_count);
mfem::Vector displacement_residual(displacement_size);
mfem::Array<mfem::Vector *> element_residual(block_count);
element_residual[velocity_block] = &velocity_residual;
element_residual[density_block] = &density_residual;
element_residual[gravity_gradient_block] = &gravity_gradient_residual;
element_residual[gravity_potential_block] = &gravity_potential_residual;
element_residual[displacement_block] = &displacement_residual;
integrators::GravityMomentumIntegrator integrator(
domain_mapper, integrators::GravityForceJacobianMode::field_coupled
);
const mfem::IntegrationRule &integration_rule =
mfem::IntRules.Get(velocity_element->GetGeomType(), 8);
integrator.SetIntegrationRule(integration_rule);
auto assemble_velocity_residual = [&](const mfem::Vector &density_state,
const mfem::Vector &gravity_state) {
element_state[density_block] = &density_state;
element_state[gravity_gradient_block] = &gravity_state;
integrator.AssembleElementVector(
elements, *transformation, element_state, element_residual
);
return mfem::Vector(velocity_residual);
};
auto scaled_difference_norm = [](mfem::Vector computed,
const mfem::Vector &reference,
const double scale) {
computed.Add(-scale, reference);
return computed.Norml2();
};
const mfem::Vector base_residual =
assemble_velocity_residual(density_dofs, gravity_gradient_dofs);
REQUIRE(base_residual.Norml2() > tolerance);
const mfem::Vector zero_field_residual =
assemble_velocity_residual(density_dofs, zero_gravity);
mfem::Vector reversed_gravity(gravity_gradient_dofs);
reversed_gravity *= -1.0;
const mfem::Vector reversed_residual =
assemble_velocity_residual(density_dofs, reversed_gravity);
mfem::Vector scaled_gravity(gravity_gradient_dofs);
scaled_gravity *= gravity_scale;
const mfem::Vector gravity_scaled_residual =
assemble_velocity_residual(density_dofs, scaled_gravity);
mfem::Vector scaled_density(density_dofs);
scaled_density *= density_scale;
const mfem::Vector density_scaled_residual =
assemble_velocity_residual(scaled_density, gravity_gradient_dofs);
const mfem::Vector jointly_scaled_residual =
assemble_velocity_residual(scaled_density, scaled_gravity);
CHECK_THAT(
zero_field_residual.Norml2(), Catch::Matchers::WithinAbs(0.0, tolerance)
);
CHECK_THAT(
scaled_difference_norm(reversed_residual, base_residual, -1.0),
Catch::Matchers::WithinAbs(0.0, tolerance)
);
CHECK_THAT(
scaled_difference_norm(
gravity_scaled_residual, base_residual, gravity_scale
),
Catch::Matchers::WithinAbs(0.0, tolerance)
);
CHECK_THAT(
scaled_difference_norm(
density_scaled_residual, base_residual, density_scale
),
Catch::Matchers::WithinAbs(0.0, tolerance)
);
CHECK_THAT(
scaled_difference_norm(
jointly_scaled_residual, base_residual,
density_scale * gravity_scale
),
Catch::Matchers::WithinAbs(0.0, tolerance)
);
auto make_test_dofs = [&](auto &&test_function) {
mfem::Vector test_dofs(velocity_size);
mfem::Vector x_physical(dim);
mfem::Vector centered_position(dim);
mfem::Vector test_value(dim);
test_dofs = 0.0;
const mfem::IntegrationRule &velocity_nodes =
velocity_element->GetNodes();
for (int i = 0; i < velocity_dofs_count; ++i) {
const mfem::IntegrationPoint &node = velocity_nodes.IntPoint(i);
transformation->SetIntPoint(&node);
domain_mapper.GetPhysicalPoint(*transformation, node, x_physical);
for (int component = 0; component < dim; ++component) {
centered_position(component) = x_physical(component) - 0.5;
}
test_function(centered_position, test_value);
for (int component = 0; component < dim; ++component) {
test_dofs(i + component * velocity_dofs_count) =
test_value(component);
}
}
return test_dofs;
};
const mfem::Vector force_x_test =
make_test_dofs([](const mfem::Vector &, mfem::Vector &value) {
value.SetSize(3);
value = 0.0;
value(0) = 1.0;
});
const mfem::Vector force_y_test =
make_test_dofs([](const mfem::Vector &, mfem::Vector &value) {
value.SetSize(3);
value = 0.0;
value(1) = 1.0;
});
const mfem::Vector force_z_test =
make_test_dofs([](const mfem::Vector &, mfem::Vector &value) {
value.SetSize(3);
value = 0.0;
value(2) = 1.0;
});
const mfem::Vector torque_x_test =
make_test_dofs([](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(3);
value(0) = 0.0;
value(1) = -position(2);
value(2) = position(1);
});
const mfem::Vector torque_y_test =
make_test_dofs([](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(3);
value(0) = position(2);
value(1) = 0.0;
value(2) = -position(0);
});
const mfem::Vector torque_z_test =
make_test_dofs([](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(3);
value(0) = -position(1);
value(1) = position(0);
value(2) = 0.0;
});
CHECK_THAT(
force_x_test * base_residual, Catch::Matchers::WithinAbs(0.0, tolerance)
);
CHECK_THAT(
force_y_test * base_residual, Catch::Matchers::WithinAbs(0.0, tolerance)
);
CHECK_THAT(
force_z_test * base_residual, Catch::Matchers::WithinAbs(0.0, tolerance)
);
CHECK_THAT(
torque_x_test * base_residual,
Catch::Matchers::WithinAbs(0.0, tolerance)
);
CHECK_THAT(
torque_y_test * base_residual,
Catch::Matchers::WithinAbs(0.0, tolerance)
);
CHECK_THAT(
torque_z_test * base_residual,
Catch::Matchers::WithinAbs(0.0, tolerance)
);
mfem::DenseMatrix dv_drho(velocity_size, density_dofs_count);
mfem::DenseMatrix dv_dgrad_phi(velocity_size, gravity_gradient_dofs_count);
mfem::Array2D<mfem::DenseMatrix *> element_matrices(
block_count, block_count
);
for (int row = 0; row < block_count; ++row) {
for (int column = 0; column < block_count; ++column) {
element_matrices(row, column) = nullptr;
}
}
element_matrices(velocity_block, density_block) = &dv_drho;
element_matrices(velocity_block, gravity_gradient_block) = &dv_dgrad_phi;
element_state[density_block] = &density_dofs;
element_state[gravity_gradient_block] = &zero_gravity;
integrator.AssembleElementGrad(
elements, *transformation, element_state, element_matrices
);
mfem::Vector zero_gravity_density_action(velocity_size);
mfem::Vector zero_gravity_field_action(velocity_size);
dv_drho.Mult(density_dofs, zero_gravity_density_action);
dv_dgrad_phi.Mult(gravity_gradient_dofs, zero_gravity_field_action);
CHECK_THAT(
zero_gravity_density_action.Norml2(),
Catch::Matchers::WithinAbs(0.0, tolerance)
);
CHECK_THAT(
scaled_difference_norm(zero_gravity_field_action, base_residual, 1.0),
Catch::Matchers::WithinAbs(0.0, tolerance)
);
element_state[density_block] = &zero_density;
element_state[gravity_gradient_block] = &gravity_gradient_dofs;
integrator.AssembleElementGrad(
elements, *transformation, element_state, element_matrices
);
mfem::Vector zero_density_density_action(velocity_size);
mfem::Vector zero_density_field_action(velocity_size);
dv_drho.Mult(density_dofs, zero_density_density_action);
dv_dgrad_phi.Mult(gravity_gradient_dofs, zero_density_field_action);
CHECK_THAT(
scaled_difference_norm(zero_density_density_action, base_residual, 1.0),
Catch::Matchers::WithinAbs(0.0, tolerance)
);
CHECK_THAT(
zero_density_field_action.Norml2(),
Catch::Matchers::WithinAbs(0.0, tolerance)
);
}

View File

@@ -0,0 +1,922 @@
#include <array>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <cmath>
#include <limits>
#include <mfem.hpp>
#include <stdexcept>
import mean_field;
import test_helpers;
using namespace mean_field;
using Catch::Matchers::WithinAbs;
namespace {
constexpr int dimension = 3;
mfem::Vector make_vector(
const double x,
const double y,
const double z
) {
mfem::Vector vector(dimension);
vector(0) = x;
vector(1) = y;
vector(2) = z;
return vector;
}
mfem::DenseMatrix make_identity() {
mfem::DenseMatrix matrix(dimension);
matrix = 0.0;
for (int i = 0; i < dimension; ++i)
matrix(i, i) = 1.0;
return matrix;
}
void check_vector(
const mfem::Vector &actual,
const mfem::Vector &expected,
const double tolerance
) {
REQUIRE(actual.Size() == expected.Size());
for (int i = 0; i < actual.Size(); ++i)
CHECK_THAT(actual(i), WithinAbs(expected(i), tolerance));
}
void check_matrix(
const mfem::DenseMatrix &actual,
const mfem::DenseMatrix &expected,
const double tolerance
) {
REQUIRE(actual.Height() == expected.Height());
REQUIRE(actual.Width() == expected.Width());
for (int i = 0; i < actual.Height(); ++i) {
for (int j = 0; j < actual.Width(); ++j)
CHECK_THAT(actual(i, j), WithinAbs(expected(i, j), tolerance));
}
}
struct AnalyticFactors {
double computational_radius;
double scale;
double scale_derivative;
};
AnalyticFactors compute_analytic_factors(
const double r_star,
const double r_inf,
const double coordinate
) {
const double radial_extent = r_inf - r_star;
const double computational_radius = r_star + coordinate * radial_extent;
const double scale =
r_star / (computational_radius * (1.0 - coordinate));
const double scale_derivative =
scale *
(1.0 / (1.0 - coordinate) - radial_extent / computational_radius);
return {
.computational_radius = computational_radius,
.scale = scale,
.scale_derivative = scale_derivative
};
}
mapping::MappingStatus evaluate_affine_map(
const mapping::compactification::ExteriorDomainMap &exterior_map,
const mfem::Vector &reference_position,
const mfem::DenseMatrix &affine_jacobian,
const mfem::Vector &offset,
const double compactification_coordinate,
const mfem::Vector &compactification_coordinate_gradient,
mapping::compactification::ExteriorMapResult &result
) {
mfem::Vector displaced_position(dimension);
affine_jacobian.Mult(reference_position, displaced_position);
displaced_position += offset;
const mapping::compactification::ExteriorMapInput input{
.reference_position = reference_position,
.displaced_position = displaced_position,
.displacement_jacobian = affine_jacobian,
.compactification_coordinate = compactification_coordinate,
.compactification_coordinate_gradient =
compactification_coordinate_gradient
};
return exterior_map.Evaluate(input, result);
}
} // namespace
TEST_CASE(
"Kelvin Compactification Validates Its Configuration",
tags::unit &tags::mapping &tags::kelvin
) {
CHECK_NOTHROW(
mapping::compactification::KelvinCompactification(
{.r_star_ref = 1.0, .r_inf_ref = 4.0}
)
);
CHECK_THROWS_AS(
mapping::compactification::KelvinCompactification(
{.r_star_ref = 0.0, .r_inf_ref = 4.0}
),
std::invalid_argument
);
CHECK_THROWS_AS(
mapping::compactification::KelvinCompactification(
{.r_star_ref = -1.0, .r_inf_ref = 4.0}
),
std::invalid_argument
);
CHECK_THROWS_AS(
mapping::compactification::KelvinCompactification(
{.r_star_ref = 2.0, .r_inf_ref = 2.0}
),
std::invalid_argument
);
CHECK_THROWS_AS(
mapping::compactification::KelvinCompactification(
{.r_star_ref = 3.0, .r_inf_ref = 2.0}
),
std::invalid_argument
);
CHECK_THROWS_AS(
mapping::compactification::KelvinCompactification(
{.r_star_ref = 1.0,
.r_inf_ref = std::numeric_limits<double>::infinity()}
),
std::invalid_argument
);
CHECK_THROWS_AS(
mapping::compactification::KelvinCompactification(
{.r_star_ref = 1.0,
.r_inf_ref = 4.0,
.coordinate_tolerance = -1.0e-12}
),
std::invalid_argument
);
CHECK_THROWS_AS(
mapping::compactification::KelvinCompactification(
{.r_star_ref = 1.0, .r_inf_ref = 4.0, .coordinate_tolerance = 1.0}
),
std::invalid_argument
);
CHECK_THROWS_AS(
mapping::compactification::KelvinCompactification(
{.r_star_ref = 1.0,
.r_inf_ref = 4.0,
.coordinate_tolerance = std::numeric_limits<double>::quiet_NaN()}
),
std::invalid_argument
);
}
TEST_CASE(
"Kelvin Compactification Reports Its Configuration",
tags::unit &tags::mapping &tags::kelvin
) {
constexpr double coordinate_tolerance = 3.0e-11;
mapping::compactification::KelvinCompactification compactification(
{.r_star_ref = 1.25,
.r_inf_ref = 5.5,
.coordinate_tolerance = coordinate_tolerance}
);
CHECK(compactification.GetName() == "KelvinCompactification");
CHECK_THAT(
compactification.GetReferenceStellarRadius(), WithinAbs(1.25, 0.0)
);
CHECK_THAT(
compactification.GetReferenceInfinityRadius(), WithinAbs(5.5, 0.0)
);
CHECK_THAT(
compactification.GetCoordinateTolerance(),
WithinAbs(coordinate_tolerance, 0.0)
);
}
TEST_CASE(
"Kelvin Compactification Uses The Exterior Coordinate Rather Than "
"Euclidean Radius",
tags::unit &tags::mapping &tags::kelvin
) {
constexpr double tolerance = 0.0;
constexpr double coordinate = 0.37;
mapping::compactification::KelvinCompactification compactification(
{.r_star_ref = 1.0, .r_inf_ref = 4.0}
);
const mfem::DenseMatrix displacement_jacobian = make_identity();
const mfem::Vector displaced_position = make_vector(1.4, -0.2, 0.3);
const mfem::Vector coordinate_gradient = make_vector(0.2, -0.1, 0.05);
const mfem::Vector reference_a = make_vector(0.2, 0.1, -0.1);
const mfem::Vector reference_b = make_vector(12.0, -7.0, 4.0);
const mapping::compactification::ExteriorMapInput input_a{
reference_a, displaced_position, displacement_jacobian, coordinate,
coordinate_gradient
};
const mapping::compactification::ExteriorMapInput input_b{
reference_b, displaced_position, displacement_jacobian, coordinate,
coordinate_gradient
};
mapping::compactification::ExteriorMapResult result_a;
mapping::compactification::ExteriorMapResult result_b;
REQUIRE(
compactification.Evaluate(input_a, result_a) ==
mapping::MappingStatus::valid
);
REQUIRE(
compactification.Evaluate(input_b, result_b) ==
mapping::MappingStatus::valid
);
check_vector(
result_a.physical_position, result_b.physical_position, tolerance
);
check_matrix(
result_a.mapping_jacobian, result_b.mapping_jacobian, tolerance
);
}
TEST_CASE(
"Kelvin Compactification Matches Its Analytic Radial Map",
tags::unit &tags::mapping &tags::kelvin
) {
constexpr double r_star = 1.0;
constexpr double r_inf = 4.0;
constexpr double radial_extent = r_inf - r_star;
constexpr double tolerance = 2.0e-12;
mapping::compactification::KelvinCompactification compactification(
{.r_star_ref = r_star, .r_inf_ref = r_inf}
);
const mfem::DenseMatrix identity = make_identity();
const mfem::Vector coordinate_gradient =
make_vector(1.0 / radial_extent, 0.0, 0.0);
for (const double computational_radius :
std::array{1.0, 1.25, 2.0, 3.0, 3.75}) {
CAPTURE(computational_radius);
const double coordinate =
(computational_radius - r_star) / radial_extent;
const mfem::Vector reference_position =
make_vector(computational_radius, 0.0, 0.0);
const mfem::Vector displaced_position(reference_position);
const mapping::compactification::ExteriorMapInput input{
reference_position, displaced_position, identity, coordinate,
coordinate_gradient
};
mapping::compactification::ExteriorMapResult result;
REQUIRE(
compactification.Evaluate(input, result) ==
mapping::MappingStatus::valid
);
const double expected_radius =
r_star * radial_extent / (r_inf - computational_radius);
const double expected_radial_derivative =
r_star * radial_extent /
std::pow(r_inf - computational_radius, 2.0);
const double expected_tangential_scale =
expected_radius / computational_radius;
const mfem::Vector expected_position =
make_vector(expected_radius, 0.0, 0.0);
mfem::DenseMatrix expected_jacobian(dimension);
expected_jacobian = 0.0;
expected_jacobian(0, 0) = expected_radial_derivative;
expected_jacobian(1, 1) = expected_tangential_scale;
expected_jacobian(2, 2) = expected_tangential_scale;
check_vector(result.physical_position, expected_position, tolerance);
check_matrix(result.mapping_jacobian, expected_jacobian, tolerance);
CHECK(result.mapping_jacobian.Det() > 0.0);
}
}
TEST_CASE(
"Kelvin Compactification Matches Its Full Cartesian Formula",
tags::unit &tags::mapping &tags::kelvin
) {
constexpr double r_star = 1.0;
constexpr double r_inf = 4.0;
constexpr double coordinate = 0.42;
constexpr double tolerance = 1.0e-12;
mapping::compactification::KelvinCompactification compactification(
{.r_star_ref = r_star, .r_inf_ref = r_inf}
);
const mfem::Vector reference_position = make_vector(0.8, 0.4, -0.2);
const mfem::Vector displaced_position = make_vector(1.1, 0.5, -0.1);
const mfem::Vector coordinate_gradient = make_vector(0.20, -0.10, 0.05);
mfem::DenseMatrix displacement_jacobian(dimension);
displacement_jacobian(0, 0) = 1.10;
displacement_jacobian(0, 1) = 0.05;
displacement_jacobian(0, 2) = 0.00;
displacement_jacobian(1, 0) = -0.02;
displacement_jacobian(1, 1) = 0.95;
displacement_jacobian(1, 2) = 0.03;
displacement_jacobian(2, 0) = 0.01;
displacement_jacobian(2, 1) = -0.04;
displacement_jacobian(2, 2) = 1.05;
const mapping::compactification::ExteriorMapInput input{
reference_position, displaced_position, displacement_jacobian,
coordinate, coordinate_gradient
};
mapping::compactification::ExteriorMapResult result;
REQUIRE(
compactification.Evaluate(input, result) ==
mapping::MappingStatus::valid
);
const AnalyticFactors factors =
compute_analytic_factors(r_star, r_inf, coordinate);
mfem::Vector expected_position(displaced_position);
expected_position *= factors.scale;
mfem::DenseMatrix expected_jacobian(displacement_jacobian);
expected_jacobian *= factors.scale;
for (int i = 0; i < dimension; ++i) {
for (int j = 0; j < dimension; ++j)
expected_jacobian(i, j) += displaced_position(i) *
factors.scale_derivative *
coordinate_gradient(j);
}
check_vector(result.physical_position, expected_position, tolerance);
check_matrix(result.mapping_jacobian, expected_jacobian, tolerance);
}
TEST_CASE(
"Kelvin Compactification Jacobian Matches Coordinate Finite Differences",
tags::unit &tags::mapping &tags::kelvin
) {
constexpr double base_coordinate = 0.38;
constexpr double difference_step = 1.0e-6;
constexpr double tolerance = 3.0e-9;
mapping::compactification::KelvinCompactification compactification(
{.r_star_ref = 1.0, .r_inf_ref = 4.0}
);
mfem::DenseMatrix affine_jacobian(dimension);
affine_jacobian(0, 0) = 1.05;
affine_jacobian(0, 1) = 0.02;
affine_jacobian(0, 2) = 0.00;
affine_jacobian(1, 0) = -0.01;
affine_jacobian(1, 1) = 0.98;
affine_jacobian(1, 2) = 0.03;
affine_jacobian(2, 0) = 0.02;
affine_jacobian(2, 1) = 0.00;
affine_jacobian(2, 2) = 1.02;
const mfem::Vector offset = make_vector(0.04, -0.03, 0.02);
const mfem::Vector reference_position = make_vector(1.4, 0.3, -0.2);
const mfem::Vector coordinate_gradient = make_vector(0.11, -0.07, 0.05);
mapping::compactification::ExteriorMapResult base_result;
REQUIRE(
evaluate_affine_map(
compactification, reference_position, affine_jacobian, offset,
base_coordinate, coordinate_gradient, base_result
) == mapping::MappingStatus::valid
);
for (int coordinate = 0; coordinate < dimension; ++coordinate) {
mfem::Vector reference_plus(reference_position);
mfem::Vector reference_minus(reference_position);
reference_plus(coordinate) += difference_step;
reference_minus(coordinate) -= difference_step;
const double compactification_plus =
base_coordinate + difference_step * coordinate_gradient(coordinate);
const double compactification_minus =
base_coordinate - difference_step * coordinate_gradient(coordinate);
mapping::compactification::ExteriorMapResult result_plus;
mapping::compactification::ExteriorMapResult result_minus;
REQUIRE(
evaluate_affine_map(
compactification, reference_plus, affine_jacobian, offset,
compactification_plus, coordinate_gradient, result_plus
) == mapping::MappingStatus::valid
);
REQUIRE(
evaluate_affine_map(
compactification, reference_minus, affine_jacobian, offset,
compactification_minus, coordinate_gradient, result_minus
) == mapping::MappingStatus::valid
);
for (int component = 0; component < dimension; ++component) {
const double finite_difference =
(result_plus.physical_position(component) -
result_minus.physical_position(component)) /
(2.0 * difference_step);
CHECK_THAT(
finite_difference,
WithinAbs(
base_result.mapping_jacobian(component, coordinate),
tolerance
)
);
}
}
}
TEST_CASE(
"Kelvin Compactification Variation Matches State Finite Differences",
tags::unit &tags::mapping &tags::kelvin
) {
constexpr double coordinate = 0.46;
constexpr double difference_step = 1.0e-6;
constexpr double tolerance = 2.0e-10;
mapping::compactification::KelvinCompactification compactification(
{.r_star_ref = 1.0, .r_inf_ref = 4.0}
);
const mfem::Vector reference_position = make_vector(1.3, -0.2, 0.4);
const mfem::Vector displaced_position = make_vector(1.4, -0.1, 0.35);
const mfem::Vector coordinate_gradient = make_vector(0.12, -0.04, 0.08);
const mfem::Vector position_direction = make_vector(0.03, -0.05, 0.02);
mfem::DenseMatrix displacement_jacobian = make_identity();
displacement_jacobian(0, 1) = 0.04;
displacement_jacobian(1, 2) = -0.03;
displacement_jacobian(2, 0) = 0.02;
mfem::DenseMatrix jacobian_direction(dimension);
jacobian_direction(0, 0) = 0.02;
jacobian_direction(0, 1) = -0.01;
jacobian_direction(0, 2) = 0.03;
jacobian_direction(1, 0) = 0.01;
jacobian_direction(1, 1) = -0.02;
jacobian_direction(1, 2) = 0.00;
jacobian_direction(2, 0) = -0.01;
jacobian_direction(2, 1) = 0.02;
jacobian_direction(2, 2) = 0.01;
const mapping::compactification::ExteriorMapInput input{
reference_position, displaced_position, displacement_jacobian,
coordinate, coordinate_gradient
};
const mapping::compactification::ExteriorMapDirection direction{
position_direction, jacobian_direction
};
mapping::compactification::ExteriorMapResult base_result;
mapping::compactification::ExteriorMapVariation variation;
REQUIRE(
compactification.Evaluate(input, base_result) ==
mapping::MappingStatus::valid
);
REQUIRE(
compactification.EvaluateVariation(
input, base_result, direction, variation
) == mapping::MappingStatus::valid
);
mfem::Vector displaced_plus(displaced_position);
mfem::Vector displaced_minus(displaced_position);
displaced_plus.Add(difference_step, position_direction);
displaced_minus.Add(-difference_step, position_direction);
mfem::DenseMatrix jacobian_plus(displacement_jacobian);
mfem::DenseMatrix jacobian_minus(displacement_jacobian);
jacobian_plus.Add(difference_step, jacobian_direction);
jacobian_minus.Add(-difference_step, jacobian_direction);
const mapping::compactification::ExteriorMapInput input_plus{
reference_position, displaced_plus, jacobian_plus, coordinate,
coordinate_gradient
};
const mapping::compactification::ExteriorMapInput input_minus{
reference_position, displaced_minus, jacobian_minus, coordinate,
coordinate_gradient
};
mapping::compactification::ExteriorMapResult result_plus;
mapping::compactification::ExteriorMapResult result_minus;
REQUIRE(
compactification.Evaluate(input_plus, result_plus) ==
mapping::MappingStatus::valid
);
REQUIRE(
compactification.Evaluate(input_minus, result_minus) ==
mapping::MappingStatus::valid
);
for (int i = 0; i < dimension; ++i) {
const double position_finite_difference =
(result_plus.physical_position(i) -
result_minus.physical_position(i)) /
(2.0 * difference_step);
CHECK_THAT(
position_finite_difference,
WithinAbs(variation.physical_position_variation(i), tolerance)
);
for (int j = 0; j < dimension; ++j) {
const double jacobian_finite_difference =
(result_plus.mapping_jacobian(i, j) -
result_minus.mapping_jacobian(i, j)) /
(2.0 * difference_step);
CHECK_THAT(
jacobian_finite_difference,
WithinAbs(variation.mapping_jacobian_variation(i, j), tolerance)
);
}
}
}
TEST_CASE(
"Kelvin Compactification Preserves Rotational Covariance",
tags::unit &tags::mapping &tags::kelvin
) {
constexpr double coordinate = 0.31;
constexpr double tolerance = 1.0e-12;
mapping::compactification::KelvinCompactification compactification(
{.r_star_ref = 1.0, .r_inf_ref = 4.0}
);
const mfem::Vector reference_position = make_vector(1.3, 0.4, -0.2);
const mfem::Vector displaced_position = make_vector(1.4, 0.2, -0.1);
const mfem::Vector coordinate_gradient = make_vector(0.16, -0.08, 0.03);
mfem::DenseMatrix displacement_jacobian = make_identity();
displacement_jacobian(0, 1) = 0.05;
displacement_jacobian(1, 0) = -0.02;
displacement_jacobian(2, 1) = 0.03;
const mapping::compactification::ExteriorMapInput input{
reference_position, displaced_position, displacement_jacobian,
coordinate, coordinate_gradient
};
mapping::compactification::ExteriorMapResult result;
REQUIRE(
compactification.Evaluate(input, result) ==
mapping::MappingStatus::valid
);
mfem::DenseMatrix rotation(dimension);
rotation = 0.0;
rotation(0, 1) = -1.0;
rotation(1, 0) = 1.0;
rotation(2, 2) = 1.0;
mfem::Vector rotated_reference(dimension);
mfem::Vector rotated_displaced(dimension);
mfem::Vector rotated_coordinate_gradient(dimension);
rotation.Mult(reference_position, rotated_reference);
rotation.Mult(displaced_position, rotated_displaced);
rotation.Mult(coordinate_gradient, rotated_coordinate_gradient);
mfem::DenseMatrix temporary(dimension);
mfem::DenseMatrix rotated_displacement_jacobian(dimension);
mfem::Mult(rotation, displacement_jacobian, temporary);
mfem::MultABt(temporary, rotation, rotated_displacement_jacobian);
const mapping::compactification::ExteriorMapInput rotated_input{
rotated_reference, rotated_displaced, rotated_displacement_jacobian,
coordinate, rotated_coordinate_gradient
};
mapping::compactification::ExteriorMapResult rotated_result;
REQUIRE(
compactification.Evaluate(rotated_input, rotated_result) ==
mapping::MappingStatus::valid
);
mfem::Vector expected_position(dimension);
rotation.Mult(result.physical_position, expected_position);
mfem::DenseMatrix expected_jacobian(dimension);
mfem::Mult(rotation, result.mapping_jacobian, temporary);
mfem::MultABt(temporary, rotation, expected_jacobian);
check_vector(
rotated_result.physical_position, expected_position, tolerance
);
check_matrix(rotated_result.mapping_jacobian, expected_jacobian, tolerance);
}
TEST_CASE(
"Kelvin Compactification Is Continuous At The Mesh Defined Stellar Surface",
tags::unit &tags::mapping &tags::kelvin
) {
constexpr double tolerance = 1.0e-14;
mapping::compactification::KelvinCompactification compactification(
{.r_star_ref = 1.0, .r_inf_ref = 4.0}
);
const mfem::Vector reference_position = make_vector(
-0.5260553366425769, 0.5260553366425769, -0.6553163792879153
);
const mfem::Vector displaced_position = make_vector(-0.55, 0.51, -0.63);
const mfem::Vector coordinate_gradient = make_vector(-0.18, 0.18, -0.22);
const mfem::DenseMatrix displacement_jacobian = make_identity();
REQUIRE(reference_position.Norml2() < 1.0);
const mapping::compactification::ExteriorMapInput input{
reference_position, displaced_position, displacement_jacobian, 0.0,
coordinate_gradient
};
mapping::compactification::ExteriorMapResult result;
REQUIRE(
compactification.Evaluate(input, result) ==
mapping::MappingStatus::valid
);
check_vector(result.physical_position, displaced_position, tolerance);
CHECK(result.mapping_jacobian.Det() > 0.0);
}
TEST_CASE(
"Kelvin Compactification Has Correct Infinity And Coordinate Bound "
"Behavior",
tags::unit &tags::mapping &tags::kelvin
) {
constexpr double r_star = 1.0;
constexpr double r_inf = 4.0;
constexpr double radial_extent = r_inf - r_star;
constexpr double coordinate_tolerance = 1.0e-12;
constexpr double tolerance = 2.0e-11;
mapping::compactification::KelvinCompactification compactification(
{.r_star_ref = r_star,
.r_inf_ref = r_inf,
.coordinate_tolerance = coordinate_tolerance}
);
const mfem::DenseMatrix identity = make_identity();
const mfem::Vector coordinate_gradient =
make_vector(1.0 / radial_extent, 0.0, 0.0);
for (const double coordinate :
std::array{0.0, 0.25, 0.75, 0.95, 0.99, 0.999}) {
CAPTURE(coordinate);
const double computational_radius = r_star + coordinate * radial_extent;
const mfem::Vector reference_position =
make_vector(computational_radius, 0.0, 0.0);
const mapping::compactification::ExteriorMapInput input{
reference_position, reference_position, identity, coordinate,
coordinate_gradient
};
mapping::compactification::ExteriorMapResult result;
REQUIRE(
compactification.Evaluate(input, result) ==
mapping::MappingStatus::valid
);
CHECK_THAT(
result.physical_position.Norml2() * (1.0 - coordinate),
WithinAbs(r_star, tolerance)
);
}
const mfem::Vector reference_position = make_vector(r_inf, 0.0, 0.0);
mapping::compactification::ExteriorMapResult result;
CHECK(
compactification.Evaluate(
{reference_position, reference_position, identity, 1.0,
coordinate_gradient},
result
) == mapping::MappingStatus::at_compactified_infinity
);
CHECK(
compactification.Evaluate(
{reference_position, reference_position, identity,
1.0 - 0.5 * coordinate_tolerance, coordinate_gradient},
result
) == mapping::MappingStatus::at_compactified_infinity
);
CHECK(
compactification.Evaluate(
{reference_position, reference_position, identity,
1.0 + 0.5 * coordinate_tolerance, coordinate_gradient},
result
) == mapping::MappingStatus::at_compactified_infinity
);
CHECK(
compactification.Evaluate(
{reference_position, reference_position, identity,
1.0 + 2.0 * coordinate_tolerance, coordinate_gradient},
result
) == mapping::MappingStatus::outside_reference_domain
);
CHECK(
compactification.Evaluate(
{reference_position, reference_position, identity,
-2.0 * coordinate_tolerance, coordinate_gradient},
result
) == mapping::MappingStatus::outside_reference_domain
);
const mfem::Vector surface_position = make_vector(0.97, 0.0, 0.0);
CHECK(
compactification.Evaluate(
{surface_position, surface_position, identity,
-0.5 * coordinate_tolerance, coordinate_gradient},
result
) == mapping::MappingStatus::valid
);
check_vector(result.physical_position, surface_position, tolerance);
}
TEST_CASE(
"Kelvin Compactification Rejects Invalid Inputs And Inverted Maps",
tags::unit &tags::mapping &tags::kelvin
) {
mapping::compactification::KelvinCompactification compactification(
{.r_star_ref = 1.0, .r_inf_ref = 4.0}
);
const mfem::Vector reference_position = make_vector(2.0, 0.0, 0.0);
const mfem::Vector displaced_position(reference_position);
const mfem::Vector coordinate_gradient = make_vector(1.0 / 3.0, 0.0, 0.0);
const mfem::Vector zero_gradient = make_vector(0.0, 0.0, 0.0);
const mfem::DenseMatrix identity = make_identity();
mapping::compactification::ExteriorMapResult result;
mfem::Vector wrong_dimension(2);
wrong_dimension = 1.0;
CHECK(
compactification.Evaluate(
{wrong_dimension, displaced_position, identity, 1.0 / 3.0,
coordinate_gradient},
result
) == mapping::MappingStatus::invalid_dimension
);
CHECK(
compactification.Evaluate(
{reference_position, displaced_position, identity, 1.0 / 3.0,
wrong_dimension},
result
) == mapping::MappingStatus::invalid_dimension
);
mfem::Vector non_finite_position(reference_position);
non_finite_position(1) = std::numeric_limits<double>::quiet_NaN();
CHECK(
compactification.Evaluate(
{non_finite_position, displaced_position, identity, 1.0 / 3.0,
coordinate_gradient},
result
) == mapping::MappingStatus::non_finite_input
);
mfem::Vector non_finite_gradient(coordinate_gradient);
non_finite_gradient(2) = std::numeric_limits<double>::infinity();
CHECK(
compactification.Evaluate(
{reference_position, displaced_position, identity, 1.0 / 3.0,
non_finite_gradient},
result
) == mapping::MappingStatus::non_finite_input
);
CHECK(
compactification.Evaluate(
{reference_position, displaced_position, identity,
std::numeric_limits<double>::quiet_NaN(), coordinate_gradient},
result
) == mapping::MappingStatus::non_finite_input
);
mfem::DenseMatrix singular_displacement_jacobian(dimension);
singular_displacement_jacobian = 0.0;
CHECK(
compactification.Evaluate(
{reference_position, displaced_position,
singular_displacement_jacobian, 1.0 / 3.0, zero_gradient},
result
) == mapping::MappingStatus::non_positive_determinant
);
mfem::DenseMatrix inverted_displacement_jacobian = make_identity();
inverted_displacement_jacobian(0, 0) = -1.0;
CHECK(
compactification.Evaluate(
{reference_position, displaced_position,
inverted_displacement_jacobian, 1.0 / 3.0, zero_gradient},
result
) == mapping::MappingStatus::non_positive_determinant
);
}
TEST_CASE(
"Kelvin Compactification Variation Rejects Invalid Inputs",
tags::unit &tags::mapping &tags::kelvin
) {
mapping::compactification::KelvinCompactification compactification(
{.r_star_ref = 1.0, .r_inf_ref = 4.0}
);
const mfem::Vector reference_position = make_vector(2.0, 0.0, 0.0);
const mfem::Vector displaced_position(reference_position);
const mfem::Vector coordinate_gradient = make_vector(1.0 / 3.0, 0.0, 0.0);
const mfem::DenseMatrix identity = make_identity();
const mapping::compactification::ExteriorMapInput input{
reference_position, displaced_position, identity, 1.0 / 3.0,
coordinate_gradient
};
mapping::compactification::ExteriorMapResult result;
REQUIRE(
compactification.Evaluate(input, result) ==
mapping::MappingStatus::valid
);
const mfem::Vector valid_position_direction =
make_vector(0.01, -0.02, 0.03);
const mfem::DenseMatrix valid_jacobian_direction = make_identity();
mapping::compactification::ExteriorMapVariation variation;
mfem::Vector wrong_dimension(2);
wrong_dimension = 0.0;
CHECK(
compactification.EvaluateVariation(
input, result, {wrong_dimension, valid_jacobian_direction},
variation
) == mapping::MappingStatus::invalid_dimension
);
mfem::DenseMatrix wrong_jacobian_dimension(2);
wrong_jacobian_dimension = 0.0;
CHECK(
compactification.EvaluateVariation(
input, result, {valid_position_direction, wrong_jacobian_dimension},
variation
) == mapping::MappingStatus::invalid_dimension
);
mfem::Vector non_finite_direction(valid_position_direction);
non_finite_direction(0) = std::numeric_limits<double>::quiet_NaN();
CHECK(
compactification.EvaluateVariation(
input, result, {non_finite_direction, valid_jacobian_direction},
variation
) == mapping::MappingStatus::non_finite_input
);
}
TEST_CASE(
"Kelvin Compactification Evaluations Are Independent",
tags::unit &tags::mapping &tags::kelvin
) {
constexpr double tolerance = 0.0;
mapping::compactification::KelvinCompactification compactification(
{.r_star_ref = 1.0, .r_inf_ref = 4.0}
);
const mfem::DenseMatrix identity = make_identity();
const mfem::Vector gradient_a = make_vector(0.12, 0.03, -0.02);
const mfem::Vector gradient_b = make_vector(-0.04, 0.15, 0.01);
const mfem::Vector reference_a = make_vector(1.5, 0.2, 0.1);
const mfem::Vector reference_b = make_vector(2.5, -0.3, 0.4);
const mapping::compactification::ExteriorMapInput input_a{
reference_a, reference_a, identity, 0.25, gradient_a
};
const mapping::compactification::ExteriorMapInput input_b{
reference_b, reference_b, identity, 0.70, gradient_b
};
mapping::compactification::ExteriorMapResult first_a;
mapping::compactification::ExteriorMapResult result_b;
mapping::compactification::ExteriorMapResult second_a;
REQUIRE(
compactification.Evaluate(input_a, first_a) ==
mapping::MappingStatus::valid
);
REQUIRE(
compactification.Evaluate(input_b, result_b) ==
mapping::MappingStatus::valid
);
REQUIRE(
compactification.Evaluate(input_a, second_a) ==
mapping::MappingStatus::valid
);
check_vector(
first_a.physical_position, second_a.physical_position, tolerance
);
check_matrix(
first_a.mapping_jacobian, second_a.mapping_jacobian, tolerance
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,454 @@
#include <array>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <cmath>
#include <limits>
#include <mfem.hpp>
#include <string>
#include <vector>
import mean_field;
import test_helpers;
using namespace mean_field;
using Catch::Matchers::WithinAbs;
namespace {
constexpr int dimension = 3;
mfem::DenseMatrix make_matrix(
const std::array<
double,
9> &values
) {
mfem::DenseMatrix matrix(dimension);
for (int row = 0; row < dimension; ++row) {
for (int column = 0; column < dimension; ++column)
matrix(row, column) = values[row * dimension + column];
}
return matrix;
}
mfem::DenseMatrix make_identity_matrix() {
mfem::DenseMatrix identity(dimension);
identity = 0.0;
for (int i = 0; i < dimension; ++i)
identity(i, i) = 1.0;
return identity;
}
mapping::MappingPointContext
make_context(const mfem::DenseMatrix &jacobian) {
mapping::MappingPointContext context;
context.mapping_jacobian = jacobian;
context.mapping_determinant = jacobian.Det();
context.inverse_mapping_jacobian.SetSize(dimension);
mfem::CalcInverse(jacobian, context.inverse_mapping_jacobian);
context.physical_position.SetSize(dimension);
context.physical_position = 0.0;
return context;
}
double determinant_variation(
const mfem::DenseMatrix &jacobian,
const mfem::DenseMatrix &jacobian_variation
) {
mfem::DenseMatrix inverse_jacobian(dimension);
mfem::DenseMatrix product(dimension);
mfem::CalcInverse(jacobian, inverse_jacobian);
mfem::Mult(inverse_jacobian, jacobian_variation, product);
double trace = 0.0;
for (int i = 0; i < dimension; ++i)
trace += product(i, i);
return jacobian.Det() * trace;
}
mapping::MappingPointVariation make_variation(
const mfem::DenseMatrix &jacobian,
const mfem::DenseMatrix &jacobian_variation
) {
mapping::MappingPointVariation variation;
variation.mapping_jacobian_variation = jacobian_variation;
variation.mapping_determinant_variation =
determinant_variation(jacobian, jacobian_variation);
variation.physical_position_variation.SetSize(dimension);
variation.physical_position_variation = 0.0;
return variation;
}
double matrix_norm(const mfem::DenseMatrix &matrix) {
double norm_squared = 0.0;
for (int row = 0; row < matrix.Height(); ++row) {
for (int column = 0; column < matrix.Width(); ++column)
norm_squared += matrix(row, column) * matrix(row, column);
}
return std::sqrt(norm_squared);
}
double relative_matrix_error(
const mfem::DenseMatrix &computed,
const mfem::DenseMatrix &reference
) {
REQUIRE(computed.Height() == reference.Height());
REQUIRE(computed.Width() == reference.Width());
mfem::DenseMatrix difference(computed);
difference -= reference;
return matrix_norm(difference) /
std::max(
matrix_norm(reference),
std::numeric_limits<double>::epsilon()
);
}
double matrix_asymmetry(const mfem::DenseMatrix &matrix) {
double asymmetry_squared = 0.0;
for (int row = 0; row < matrix.Height(); ++row) {
for (int column = 0; column < matrix.Width(); ++column) {
const double difference =
matrix(row, column) - matrix(column, row);
asymmetry_squared += difference * difference;
}
}
return std::sqrt(asymmetry_squared);
}
mfem::DenseMatrix centered_mass_tensor_difference(
const mfem::DenseMatrix &jacobian,
const mfem::DenseMatrix &jacobian_variation,
const double step
) {
mfem::DenseMatrix plus_jacobian(jacobian);
mfem::DenseMatrix minus_jacobian(jacobian);
for (int row = 0; row < dimension; ++row) {
for (int column = 0; column < dimension; ++column) {
plus_jacobian(row, column) +=
step * jacobian_variation(row, column);
minus_jacobian(row, column) -=
step * jacobian_variation(row, column);
}
}
REQUIRE(plus_jacobian.Det() > 0.0);
REQUIRE(minus_jacobian.Det() > 0.0);
const mapping::MappingPointContext plus_context =
make_context(plus_jacobian);
const mapping::MappingPointContext minus_context =
make_context(minus_jacobian);
mfem::DenseMatrix plus_tensor;
mfem::DenseMatrix minus_tensor;
mapping::ComputeHDivMassTensor(plus_context, plus_tensor);
mapping::ComputeHDivMassTensor(minus_context, minus_tensor);
plus_tensor -= minus_tensor;
plus_tensor *= 1 / (2.0 * step);
return plus_tensor;
}
void check_zero_matrix(
const mfem::DenseMatrix &matrix,
const double tolerance
) {
for (int row = 0; row < matrix.Height(); ++row) {
for (int column = 0; column < matrix.Width(); ++column)
CHECK_THAT(matrix(row, column), WithinAbs(0.0, tolerance));
}
}
struct TensorVariationCase {
std::string name;
mfem::DenseMatrix jacobian;
mfem::DenseMatrix jacobian_variation;
};
} // namespace
TEST_CASE(
"Hdiv Mass Tensor Variation Matches Centered Differences",
tags::unit &tags::transformations
) {
std::vector<TensorVariationCase> cases;
cases.push_back(
{"identity with general variation", make_identity_matrix(),
make_matrix({0.12, -0.07, 0.03, 0.05, -0.09, 0.04, -0.02, 0.08, 0.06})}
);
cases.push_back(
{"anisotropic stretch",
make_matrix({1.20, 0.00, 0.00, 0.00, 0.85, 0.00, 0.00, 0.00, 1.10}),
make_matrix({0.08, 0.01, -0.03, 0.02, -0.05, 0.04, 0.01, -0.02, 0.07})}
);
cases.push_back(
{"sheared mapping",
make_matrix({1.10, 0.20, -0.05, 0.04, 0.90, 0.12, -0.03, 0.08, 1.15}),
make_matrix(
{0.06, -0.04, 0.02, 0.03, 0.05, -0.07, -0.01, 0.04, -0.02}
)}
);
cases.push_back(
{"strong general mapping",
make_matrix({1.35, 0.31, -0.18, -0.12, 0.78, 0.22, 0.09, -0.16, 1.27}),
make_matrix({-0.11, 0.08, 0.05, 0.07, 0.09, -0.04, -0.06, 0.03, 0.12})}
);
for (const TensorVariationCase &test_case : cases) {
DYNAMIC_SECTION(test_case.name) {
REQUIRE(test_case.jacobian.Det() > 0.0);
const mapping::MappingPointContext context =
make_context(test_case.jacobian);
const mapping::MappingPointVariation variation = make_variation(
test_case.jacobian, test_case.jacobian_variation
);
mfem::DenseMatrix analytic_variation;
mapping::ComputeHDivMassTensorVariation(
context, variation, analytic_variation
);
const mfem::DenseMatrix finite_difference =
centered_mass_tensor_difference(
test_case.jacobian, test_case.jacobian_variation, 1.0e-6
);
const double relative_error =
relative_matrix_error(analytic_variation, finite_difference);
const double asymmetry = matrix_asymmetry(analytic_variation);
INFO("Mapping determinant = " << context.mapping_determinant);
INFO(
"Determinant variation = "
<< variation.mapping_determinant_variation
);
INFO(
"Analytic variation norm = " << matrix_norm(analytic_variation)
);
INFO(
"Finite-difference variation norm = "
<< matrix_norm(finite_difference)
);
INFO("Relative tensor-variation error = " << relative_error);
INFO("Tensor-variation asymmetry = " << asymmetry);
CHECK(relative_error < 2.0e-9);
CHECK(asymmetry < 2.0e-14);
}
}
}
TEST_CASE(
"Hdiv Mass Tensor Variation Has Second Order Centered Difference "
"Convergence",
tags::unit &tags::transformations &tags::convergence
) {
const mfem::DenseMatrix jacobian =
make_matrix({1.18, 0.17, -0.09, -0.04, 0.92, 0.14, 0.07, -0.11, 1.23});
const mfem::DenseMatrix jacobian_variation =
make_matrix({0.09, -0.06, 0.04, 0.03, 0.07, -0.05, -0.02, 0.08, -0.03});
const mapping::MappingPointContext context = make_context(jacobian);
const mapping::MappingPointVariation variation =
make_variation(jacobian, jacobian_variation);
mfem::DenseMatrix analytic_variation;
mapping::ComputeHDivMassTensorVariation(
context, variation, analytic_variation
);
const std::array<double, 3> steps{4.0e-2, 2.0e-2, 1.0e-2};
std::array<double, 3> errors{};
for (int i = 0; i < static_cast<int>(steps.size()); ++i) {
const mfem::DenseMatrix finite_difference =
centered_mass_tensor_difference(
jacobian, jacobian_variation, steps[i]
);
errors[i] =
relative_matrix_error(finite_difference, analytic_variation);
INFO("Step = " << steps[i] << ", relative error = " << errors[i]);
}
const double first_reduction = errors[1] / errors[0];
const double second_reduction = errors[2] / errors[1];
INFO("First error-reduction ratio = " << first_reduction);
INFO("Second error-reduction ratio = " << second_reduction);
CHECK(first_reduction < 0.30);
CHECK(second_reduction < 0.30);
CHECK(errors[2] < 1.0e-5);
}
TEST_CASE(
"Hdiv Mass Tensor Variation Vanishes For Translation",
tags::unit &tags::transformations
) {
const mfem::DenseMatrix jacobian =
make_matrix({1.12, 0.08, -0.03, 0.02, 0.94, 0.07, -0.01, 0.05, 1.09});
mfem::DenseMatrix zero_jacobian_variation(dimension);
zero_jacobian_variation = 0.0;
mapping::MappingPointContext context = make_context(jacobian);
mapping::MappingPointVariation variation =
make_variation(jacobian, zero_jacobian_variation);
variation.physical_position_variation.SetSize(dimension);
variation.physical_position_variation(0) = 0.7;
variation.physical_position_variation(1) = -0.4;
variation.physical_position_variation(2) = 0.9;
mfem::DenseMatrix tensor_variation;
mapping::ComputeHDivMassTensorVariation(
context, variation, tensor_variation
);
CHECK_THAT(variation.mapping_determinant_variation, WithinAbs(0.0, 0.0));
check_zero_matrix(tensor_variation, 1.0e-14);
}
TEST_CASE(
"Hdiv Mass Tensor Variation Vanishes For Infinitesimal Rotation At "
"Identity",
tags::unit &tags::transformations
) {
const mfem::DenseMatrix identity = make_identity_matrix();
const mfem::DenseMatrix rotation_variation =
make_matrix({0.0, -0.30, 0.20, 0.30, 0.0, -0.15, -0.20, 0.15, 0.0});
const mapping::MappingPointContext context = make_context(identity);
const mapping::MappingPointVariation variation =
make_variation(identity, rotation_variation);
mfem::DenseMatrix tensor_variation;
mapping::ComputeHDivMassTensorVariation(
context, variation, tensor_variation
);
CHECK_THAT(
variation.mapping_determinant_variation, WithinAbs(0.0, 1.0e-15)
);
check_zero_matrix(tensor_variation, 1.0e-14);
}
TEST_CASE(
"Hdiv Mass Tensor Variation Matches Isotropic Scaling At Identity",
tags::unit &tags::transformations
) {
constexpr double scaling_variation = 0.17;
const mfem::DenseMatrix identity = make_identity_matrix();
mfem::DenseMatrix jacobian_variation(dimension);
jacobian_variation = 0.0;
for (int i = 0; i < dimension; ++i)
jacobian_variation(i, i) = scaling_variation;
const mapping::MappingPointContext context = make_context(identity);
const mapping::MappingPointVariation variation =
make_variation(identity, jacobian_variation);
mfem::DenseMatrix tensor_variation;
mapping::ComputeHDivMassTensorVariation(
context, variation, tensor_variation
);
CHECK_THAT(
variation.mapping_determinant_variation,
WithinAbs(3.0 * scaling_variation, 1.0e-14)
);
for (int row = 0; row < dimension; ++row) {
for (int column = 0; column < dimension; ++column) {
const double expected = row == column ? -scaling_variation : 0.0;
CHECK_THAT(
tensor_variation(row, column), WithinAbs(expected, 1.0e-14)
);
}
}
}
TEST_CASE(
"Hdiv Mass Tensor Variation Symmetrizes Simple Shear At Identity",
tags::unit &tags::transformations
) {
constexpr double shear_variation = 0.23;
const mfem::DenseMatrix identity = make_identity_matrix();
mfem::DenseMatrix jacobian_variation(dimension);
jacobian_variation = 0.0;
jacobian_variation(0, 1) = shear_variation;
const mapping::MappingPointContext context = make_context(identity);
const mapping::MappingPointVariation variation =
make_variation(identity, jacobian_variation);
mfem::DenseMatrix tensor_variation;
mapping::ComputeHDivMassTensorVariation(
context, variation, tensor_variation
);
CHECK_THAT(
variation.mapping_determinant_variation, WithinAbs(0.0, 1.0e-15)
);
CHECK_THAT(tensor_variation(0, 1), WithinAbs(shear_variation, 1.0e-14));
CHECK_THAT(tensor_variation(1, 0), WithinAbs(shear_variation, 1.0e-14));
for (int row = 0; row < dimension; ++row) {
for (int column = 0; column < dimension; ++column) {
if ((row == 0 && column == 1) || (row == 1 && column == 0))
continue;
CHECK_THAT(tensor_variation(row, column), WithinAbs(0.0, 1.0e-14));
}
}
}
TEST_CASE(
"Mapping Determinant Variation Matches Jacobi Formula",
tags::unit &tags::transformations
) {
const mfem::DenseMatrix jacobian =
make_matrix({1.24, 0.19, -0.07, -0.06, 0.88, 0.16, 0.04, -0.12, 1.19});
const mfem::DenseMatrix jacobian_variation =
make_matrix({0.08, -0.03, 0.05, 0.02, 0.06, -0.04, -0.01, 0.07, -0.02});
const mapping::MappingPointContext context = make_context(jacobian);
const mapping::MappingPointVariation variation =
make_variation(jacobian, jacobian_variation);
constexpr double difference_step = 1.0e-3;
mfem::DenseMatrix plus_one(context.mapping_jacobian);
mfem::DenseMatrix plus_two(context.mapping_jacobian);
mfem::DenseMatrix minus_one(context.mapping_jacobian);
mfem::DenseMatrix minus_two(context.mapping_jacobian);
plus_one.Add(difference_step, variation.mapping_jacobian_variation);
plus_two.Add(2.0 * difference_step, variation.mapping_jacobian_variation);
minus_one.Add(-difference_step, variation.mapping_jacobian_variation);
minus_two.Add(-2.0 * difference_step, variation.mapping_jacobian_variation);
const double finite_difference = (minus_two.Det() - 8.0 * minus_one.Det() +
8.0 * plus_one.Det() - plus_two.Det()) /
(12.0 * difference_step);
const double analytic = variation.mapping_determinant_variation;
const double relative_error = std::abs(finite_difference - analytic) /
std::max(std::abs(analytic), 1.0e-14);
INFO("Analytic determinant variation = " << analytic);
INFO("Finite-difference determinant variation = " << finite_difference);
INFO("Relative determinant-variation error = " << relative_error);
CHECK(relative_error < 2.0e-11);
}

View File

@@ -0,0 +1,297 @@
#include <cstdint>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
import mean_field;
import test_helpers;
namespace barotropic_closure_context_test_utils {
mfem::Vector project_field(
mfem::ParFiniteElementSpace &finiteElementSpace,
mfem::Coefficient &coefficient
) {
mfem::ParGridFunction field(&finiteElementSpace);
field.ProjectCoefficient(coefficient);
mfem::Vector trueVector;
field.GetTrueDofs(trueVector);
return trueVector;
}
mfem::Vector make_density(const mean_field::fem::FEM &f) {
mfem::FunctionCoefficient coefficient([](const mfem::Vector &position) {
return 0.55 + 0.025 * position(0) - 0.010 * position(1);
});
return project_field(*f.densityFes, coefficient);
}
mfem::Vector make_enthalpy(const mean_field::fem::FEM &f) {
mfem::FunctionCoefficient coefficient([](const mfem::Vector &position) {
return 0.90 + 0.020 * position(0) - 0.010 * position(1) +
0.005 * position(2);
});
return project_field(*f.enthalpyFes, coefficient);
}
} // namespace barotropic_closure_context_test_utils
TEST_CASE(
"Barotropic Closure Context Tracks Independent Revisions",
tags::barotrope &tags::closure &tags::hydro &tags::prepared &tags::unit
) {
auto args = test_utils::setup_args();
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
const mean_field::physics::PolytropicBarotrope barotrope(3.0, 1.5);
mean_field::operators::context::barotropic::
BarotropicClosureLinearizationContext context(
f, *f.domainMapperStateless, barotrope
);
mfem::Vector density =
barotropic_closure_context_test_utils::make_density(f);
mfem::Vector enthalpy =
barotropic_closure_context_test_utils::make_enthalpy(f);
mfem::Vector displacement =
gravity_prepared_test_utils::make_displacement(f, 0.0);
mean_field::operators::context::barotropic::BarotropicClosureRevisions
revisions{.density = 3, .enthalpy = 5, .displacement = 7};
CHECK_FALSE(context.IsPrepared());
CHECK(context.GetPreparationCount() == 0);
context.Prepare(density, enthalpy, displacement, revisions);
REQUIRE(context.IsPrepared());
CHECK(context.MatchesRevisions(revisions));
CHECK(context.GetRevisions() == revisions);
CHECK(context.GetPreparationCount() == 1);
CHECK(context.GetOperator().GetPreparationCount() == 1);
context.Prepare(density, enthalpy, displacement, revisions);
CHECK(context.GetPreparationCount() == 1);
const double frozenDensityValue = context.GetBaseDensityTrue()(0);
density(0) += 0.125;
CHECK(context.GetBaseDensityTrue()(0) == frozenDensityValue);
context.Prepare(density, enthalpy, displacement, revisions);
CHECK(context.GetPreparationCount() == 1);
CHECK(context.GetBaseDensityTrue()(0) == frozenDensityValue);
++revisions.density;
context.Prepare(density, enthalpy, displacement, revisions);
CHECK(context.GetPreparationCount() == 2);
CHECK(context.GetBaseDensityTrue()(0) == density(0));
enthalpy(0) += 0.050;
++revisions.enthalpy;
context.Prepare(density, enthalpy, displacement, revisions);
CHECK(context.GetPreparationCount() == 3);
CHECK(context.GetBaseEnthalpyTrue()(0) == enthalpy(0));
displacement = gravity_prepared_test_utils::make_displacement(f, 1.0);
++revisions.displacement;
context.Prepare(density, enthalpy, displacement, revisions);
CHECK(context.GetPreparationCount() == 4);
CHECK(context.GetRevisions() == revisions);
CHECK(context.MatchesRevisions(revisions));
CHECK(context.GetOperator().GetPreparationCount() == 4);
}
TEST_CASE(
"Barotropic Closure Context Reprepares A Consistent Frozen State",
tags::barotrope &tags::closure &tags::hydro &tags::prepared &tags::unit
) {
auto args = test_utils::setup_args();
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
const mean_field::physics::PolytropicBarotrope barotrope(3.0, 1.5);
mean_field::operators::context::barotropic::
BarotropicClosureLinearizationContext context(
f, *f.domainMapperStateless, barotrope
);
const mfem::Vector density =
barotropic_closure_context_test_utils::make_density(f);
const mfem::Vector enthalpy =
barotropic_closure_context_test_utils::make_enthalpy(f);
const mfem::Vector identityDisplacement =
gravity_prepared_test_utils::make_displacement(f, 0.0);
const mfem::Vector densityVariation =
gravity_prepared_test_utils::make_deterministic_vector(
f.densityFes->GetTrueVSize(), 0.37
);
const mfem::Vector enthalpyVariation =
gravity_prepared_test_utils::make_deterministic_vector(
f.enthalpyFes->GetTrueVSize(), 0.71
);
const mfem::Vector displacementVariation =
gravity_prepared_test_utils::make_deterministic_vector(
f.displacementFes->GetTrueVSize(), 0.37
);
mean_field::operators::context::barotropic::BarotropicClosureRevisions
revisions{.density = 11, .enthalpy = 13, .displacement = 17};
context.Prepare(density, enthalpy, identityDisplacement, revisions);
mfem::Vector initialResidual;
mfem::Vector initialAction;
context.BuildResidual(initialResidual);
context.GetOperator().Mult(
densityVariation, enthalpyVariation, displacementVariation,
initialAction
);
mfem::Vector changedDensity(density);
changedDensity.Add(0.025, densityVariation);
mfem::Vector changedEnthalpy(enthalpy);
changedEnthalpy.Add(0.015, enthalpyVariation);
const mfem::Vector deformedDisplacement =
gravity_prepared_test_utils::make_displacement(f, 1.0);
context.Prepare(
changedDensity, changedEnthalpy, deformedDisplacement, revisions
);
mfem::Vector unchangedResidual;
mfem::Vector unchangedAction;
context.BuildResidual(unchangedResidual);
context.GetOperator().Mult(
densityVariation, enthalpyVariation, displacementVariation,
unchangedAction
);
const MPI_Comm communicator = f.mesh->GetComm();
CHECK(context.GetPreparationCount() == 1);
CHECK(
gravity_prepared_test_utils::relative_error(
unchangedResidual, initialResidual, communicator
) < 5.0e-15
);
CHECK(
gravity_prepared_test_utils::relative_error(
unchangedAction, initialAction, communicator
) < 5.0e-15
);
++revisions.density;
++revisions.enthalpy;
++revisions.displacement;
context.Prepare(
changedDensity, changedEnthalpy, deformedDisplacement, revisions
);
mfem::Vector preparedResidual;
mfem::Vector preparedAction;
context.BuildResidual(preparedResidual);
context.GetOperator().Mult(
densityVariation, enthalpyVariation, displacementVariation,
preparedAction
);
mfem::Vector referenceResidual;
mfem::Vector referenceDensityAction;
mfem::Vector referenceEnthalpyAction;
mfem::Vector referenceDisplacementAction;
mean_field::operators::kernels::apply_barotropic_closure(
f, *f.domainMapperStateless, barotrope, changedDensity, changedEnthalpy,
deformedDisplacement, referenceResidual
);
mean_field::operators::kernels::apply_barotropic_closure_density_action(
f, *f.domainMapperStateless, barotrope, densityVariation,
deformedDisplacement, referenceDensityAction
);
mean_field::operators::kernels::apply_barotropic_closure_enthalpy_action(
f, *f.domainMapperStateless, barotrope, changedEnthalpy,
enthalpyVariation, deformedDisplacement, referenceEnthalpyAction
);
mean_field::operators::kernels::
apply_barotropic_closure_displacement_action(
f, *f.domainMapperStateless, barotrope, changedDensity,
changedEnthalpy, deformedDisplacement, displacementVariation,
referenceDisplacementAction
);
mfem::Vector referenceAction(referenceDensityAction);
referenceAction += referenceEnthalpyAction;
referenceAction += referenceDisplacementAction;
const double residualError = gravity_prepared_test_utils::relative_error(
preparedResidual, referenceResidual, communicator
);
const double actionError = gravity_prepared_test_utils::relative_error(
preparedAction, referenceAction, communicator
);
const double residualChange = gravity_prepared_test_utils::relative_error(
preparedResidual, initialResidual, communicator
);
const double actionChange = gravity_prepared_test_utils::relative_error(
preparedAction, initialAction, communicator
);
INFO("Prepared-context residual error = " << residualError);
INFO("Prepared-context Jacobian error = " << actionError);
INFO("Residual change after valid revision = " << residualChange);
INFO("Jacobian change after valid revision = " << actionChange);
CHECK(context.GetPreparationCount() == 2);
CHECK(residualError < 5.0e-12);
CHECK(actionError < 5.0e-12);
CHECK(residualChange > 1.0e-6);
CHECK(actionChange > 1.0e-6);
}

View File

@@ -0,0 +1,306 @@
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
import mean_field;
import test_helpers;
using namespace mean_field;
namespace prepared_test = gravity_prepared_test_utils;
namespace gravity_context = operators::context::gravity_field;
TEST_CASE(
"Gravity Field Linearization Context Applies Selective Invalidation",
tags::integration &tags::gravity &tags::contexts
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
gravity_context::GravityFieldLinearizationContext context(
f, *f.domainMapperStateless
);
mfem::Vector density = prepared_test::make_deterministic_vector(
f.densityFes->GetTrueVSize(), 0.11
);
mfem::Vector displacement = prepared_test::make_displacement(f, 0.0);
mfem::Vector gravity_gradient = prepared_test::make_deterministic_vector(
f.gravityFluxFes->GetTrueVSize(), 0.37
);
mfem::Vector gravity_potential = prepared_test::make_deterministic_vector(
f.gravityPotentialFes->GetTrueVSize(), 0.63
);
gravity_context::GravityFieldRevisions revisions;
auto make_state = [&]() {
return gravity_context::GravityFieldStateView{
.density = density,
.displacement = displacement,
.gravity_gradient = gravity_gradient,
.gravity_potential = gravity_potential
};
};
REQUIRE_FALSE(context.IsPrepared());
const gravity_context::GravityFieldPreparationReport initial_report =
context.Prepare(make_state(), revisions);
REQUIRE(context.IsPrepared());
CHECK(initial_report.geometry.reconstructed_operators);
CHECK(initial_report.geometry.rebuilt_mass_operator);
CHECK(initial_report.geometry.rebuilt_source_operator);
CHECK(initial_report.geometry.refreshed_variation_state);
CHECK(initial_report.updated_density);
CHECK(initial_report.updated_gravity_gradient);
CHECK(initial_report.DidAnyWork());
const auto initial_mass_preparations =
context.GetGeometryContext().GetMassOperator().GetPreparationCount();
const auto initial_source_preparations =
context.GetGeometryContext().GetSourceOperator().GetPreparationCount();
const gravity_context::GravityFieldPreparationReport repeated_report =
context.Prepare(make_state(), revisions);
CHECK_FALSE(repeated_report.DidAnyWork());
CHECK(
context.GetGeometryContext().GetMassOperator().GetPreparationCount() ==
initial_mass_preparations
);
CHECK(
context.GetGeometryContext()
.GetSourceOperator()
.GetPreparationCount() == initial_source_preparations
);
gravity_potential(0) += 0.25;
++revisions.gravity_potential.value;
const gravity_context::GravityFieldPreparationReport potential_report =
context.Prepare(make_state(), revisions);
CHECK_FALSE(potential_report.DidAnyWork());
CHECK(
context.GetRevisions().gravity_potential == revisions.gravity_potential
);
density(0) += 0.5;
++revisions.density.value;
const gravity_context::GravityFieldPreparationReport density_report =
context.Prepare(make_state(), revisions);
CHECK(density_report.updated_density);
CHECK_FALSE(density_report.updated_gravity_gradient);
CHECK_FALSE(density_report.geometry.DidAnyWork());
CHECK(context.GetDensity()(0) == density(0));
gravity_gradient(0) -= 0.4;
++revisions.gravity_gradient.value;
const gravity_context::GravityFieldPreparationReport gradient_report =
context.Prepare(make_state(), revisions);
CHECK_FALSE(gradient_report.updated_density);
CHECK(gradient_report.updated_gravity_gradient);
CHECK_FALSE(gradient_report.geometry.DidAnyWork());
CHECK(context.GetGravityGradient()(0) == gravity_gradient(0));
displacement = prepared_test::make_displacement(f, 1.0);
++revisions.displacement.value;
const gravity_context::GravityFieldPreparationReport displacement_report =
context.Prepare(make_state(), revisions);
CHECK_FALSE(displacement_report.geometry.reconstructed_operators);
CHECK(displacement_report.geometry.rebuilt_mass_operator);
CHECK(displacement_report.geometry.rebuilt_source_operator);
CHECK(displacement_report.geometry.refreshed_variation_state);
CHECK_FALSE(displacement_report.updated_density);
CHECK_FALSE(displacement_report.updated_gravity_gradient);
CHECK(
context.GetGeometryContext().GetMassOperator().GetPreparationCount() ==
initial_mass_preparations + 1
);
CHECK(
context.GetGeometryContext()
.GetSourceOperator()
.GetPreparationCount() == initial_source_preparations + 1
);
++revisions.discretization.value;
const gravity_context::GravityFieldPreparationReport discretization_report =
context.Prepare(make_state(), revisions);
CHECK(discretization_report.geometry.reconstructed_operators);
CHECK(discretization_report.geometry.rebuilt_mass_operator);
CHECK(discretization_report.geometry.rebuilt_source_operator);
CHECK(discretization_report.updated_density);
CHECK(discretization_report.updated_gravity_gradient);
CHECK(
context.GetGeometryContext().GetMassOperator().GetPreparationCount() ==
1
);
CHECK(
context.GetGeometryContext()
.GetSourceOperator()
.GetPreparationCount() == 1
);
}
TEST_CASE(
"Gravity Field Linearization Context Owns Frozen Base Fields",
tags::integration &tags::gravity &tags::contexts
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
gravity_context::GravityFieldLinearizationContext context(
f, *f.domainMapperStateless
);
mfem::Vector density = prepared_test::make_deterministic_vector(
f.densityFes->GetTrueVSize(), 0.13
);
mfem::Vector displacement = prepared_test::make_displacement(f, 0.4);
mfem::Vector gravity_gradient = prepared_test::make_deterministic_vector(
f.gravityFluxFes->GetTrueVSize(), 0.47
);
mfem::Vector gravity_potential = prepared_test::make_deterministic_vector(
f.gravityPotentialFes->GetTrueVSize(), 0.71
);
gravity_context::GravityFieldRevisions revisions;
context.Prepare(
{.density = density,
.displacement = displacement,
.gravity_gradient = gravity_gradient,
.gravity_potential = gravity_potential},
revisions
);
const mfem::Vector frozen_density = context.GetDensity();
const mfem::Vector frozen_displacement =
context.GetGeometryContext().GetDisplacement();
const mfem::Vector frozen_gravity_gradient = context.GetGravityGradient();
density = 0.0;
displacement = 0.0;
gravity_gradient = 0.0;
gravity_potential = 0.0;
CHECK(
prepared_test::relative_error(
context.GetDensity(), frozen_density, f.mesh->GetComm()
) == 0.0
);
CHECK(
prepared_test::relative_error(
context.GetGeometryContext().GetDisplacement(), frozen_displacement,
f.displacementFes->GetComm()
) == 0.0
);
CHECK(
prepared_test::relative_error(
context.GetGravityGradient(), frozen_gravity_gradient,
f.gravityFluxFes->GetComm()
) == 0.0
);
const gravity_context::GravityFieldPreparationReport
unchanged_revision_report = context.Prepare(
{.density = density,
.displacement = displacement,
.gravity_gradient = gravity_gradient,
.gravity_potential = gravity_potential},
revisions
);
CHECK_FALSE(unchanged_revision_report.DidAnyWork());
CHECK(
prepared_test::relative_error(
context.GetDensity(), frozen_density, f.mesh->GetComm()
) == 0.0
);
CHECK(
prepared_test::relative_error(
context.GetGeometryContext().GetDisplacement(), frozen_displacement,
f.displacementFes->GetComm()
) == 0.0
);
CHECK(
prepared_test::relative_error(
context.GetGravityGradient(), frozen_gravity_gradient,
f.gravityFluxFes->GetComm()
) == 0.0
);
}
TEST_CASE(
"Gravity Field Geometry Contexts Have Independent Prepared State",
tags::integration &tags::gravity &tags::contexts
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
gravity_context::GravityFieldGeometryContext first_context(
f, *f.domainMapperStateless
);
gravity_context::GravityFieldGeometryContext second_context(
f, *f.domainMapperStateless
);
const mfem::Vector first_displacement =
prepared_test::make_displacement(f, 0.0);
const mfem::Vector second_displacement =
prepared_test::make_displacement(f, 1.0);
const mfem::Vector gravity_gradient =
prepared_test::make_deterministic_vector(
f.gravityFluxFes->GetTrueVSize(), 0.35
);
first_context.Prepare(first_displacement, {.value = 0}, {.value = 0});
second_context.Prepare(second_displacement, {.value = 0}, {.value = 0});
mfem::Vector first_action;
mfem::Vector second_action_before;
mfem::Vector second_action_after;
first_context.GetMassOperator().Mult(gravity_gradient, first_action);
second_context.GetMassOperator().Mult(
gravity_gradient, second_action_before
);
const mfem::Vector updated_first_displacement =
prepared_test::make_displacement(f, 0.6);
first_context.Prepare(
updated_first_displacement, {.value = 0}, {.value = 1}
);
second_context.GetMassOperator().Mult(
gravity_gradient, second_action_after
);
const MPI_Comm communicator = f.gravityFluxFes->GetComm();
const double independent_context_error = prepared_test::relative_error(
second_action_after, second_action_before, communicator
);
const double distinct_geometry_difference = prepared_test::relative_error(
first_action, second_action_before, communicator
);
INFO(
"Second-context change after preparing first context = "
<< independent_context_error
);
INFO(
"Difference between independently prepared geometries = "
<< distinct_geometry_difference
);
CHECK(independent_context_error < 2.0e-14);
CHECK(distinct_geometry_difference > 1.0e-5);
}

View File

@@ -0,0 +1,382 @@
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
import mean_field;
import test_helpers;
namespace hydrostatic_context_test_utils {
mean_field::operators::context::hydrostatic::
HydrostaticEquilibriumDependencies
make_dependencies() {
return {
.discretization = {.identity = 101, .revision = 2},
.enthalpy = {.identity = 103, .revision = 3},
.gravityPotential = {.identity = 107, .revision = 5},
.displacement = {.identity = 109, .revision = 7},
.rotation = {.identity = 113, .revision = 11},
.bernoulliConstant = {.identity = 127, .revision = 13}
};
}
mean_field::operators::context::hydrostatic::HydrostaticEquilibriumStateView
make_state(
const mfem::Vector &enthalpy,
const mfem::Vector &gravityPotential,
const mfem::Vector &displacement,
const double bernoulliConstant
) {
return {
.enthalpy = enthalpy,
.gravityPotential = gravityPotential,
.displacement = displacement,
.bernoulliConstant = bernoulliConstant
};
}
void check_base_only(
const mean_field::operators::context::hydrostatic::
HydrostaticPreparationReport &report
) {
CHECK_FALSE(report.preparedStaticDependencies);
CHECK_FALSE(report.preparedGeometryState);
CHECK_FALSE(report.preparedRotationDependencies);
CHECK(report.preparedBaseState);
}
} // namespace hydrostatic_context_test_utils
TEST_CASE(
"Hydrostatic Context Applies Selective Invalidation",
tags::barotrope &tags::contexts &tags::hydro &tags::prepared &tags::unit
) {
auto args = test_utils::setup_args();
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
mean_field::operators::context::hydrostatic::HydrostaticEquilibriumContext
context(f, *f.domainMapperStateless);
mfem::Vector enthalpy =
gravity_prepared_test_utils::make_deterministic_vector(
f.enthalpyFes->GetTrueVSize(), 0.17
);
mfem::Vector gravityPotential =
gravity_prepared_test_utils::make_deterministic_vector(
f.gravityPotentialFes->GetTrueVSize(), 0.31
);
mfem::Vector displacement =
gravity_prepared_test_utils::make_displacement(f, 0.35);
double bernoulliConstant = 0.73;
mean_field::operators::context::hydrostatic::
HydrostaticEquilibriumDependencies dependencies =
hydrostatic_context_test_utils::make_dependencies();
CHECK_FALSE(context.IsPrepared());
CHECK_FALSE(context.MatchesDependencies(dependencies));
const auto initialStatistics = context.GetPreparationStatistics();
CHECK(initialStatistics.staticPreparations == 0);
CHECK(initialStatistics.geometryPreparations == 0);
CHECK(initialStatistics.rotationPreparations == 0);
CHECK(initialStatistics.baseStatePreparations == 0);
const auto initialReport = context.Prepare(
hydrostatic_context_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
dependencies
);
REQUIRE(context.IsPrepared());
CHECK(context.MatchesDependencies(dependencies));
CHECK(context.GetDependencies() == dependencies);
CHECK(initialReport.preparedStaticDependencies);
CHECK(initialReport.preparedGeometryState);
CHECK(initialReport.preparedRotationDependencies);
CHECK(initialReport.preparedBaseState);
CHECK(initialReport.updatedEnthalpy);
CHECK(initialReport.updatedGravityPotential);
CHECK(initialReport.updatedDisplacement);
CHECK(initialReport.updatedBernoulliConstant);
CHECK(initialReport.DidAnyWork());
const mfem::Vector frozenEnthalpy = context.GetBaseEnthalpyTrue();
const mfem::Vector frozenGravityPotential =
context.GetBaseGravityPotentialTrue();
const mfem::Vector frozenDisplacement = context.GetDisplacementTrue();
const double frozenBernoulliConstant = context.GetBernoulliConstant();
enthalpy(0) += 0.125;
gravityPotential(0) -= 0.075;
displacement(0) += 0.050;
bernoulliConstant += 0.20;
const auto repeatedReport = context.Prepare(
hydrostatic_context_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
dependencies
);
CHECK_FALSE(repeatedReport.DidAnyWork());
CHECK_FALSE(repeatedReport.updatedEnthalpy);
CHECK_FALSE(repeatedReport.updatedGravityPotential);
CHECK_FALSE(repeatedReport.updatedDisplacement);
CHECK_FALSE(repeatedReport.updatedBernoulliConstant);
const MPI_Comm communicator = f.mesh->GetComm();
CHECK(
gravity_prepared_test_utils::relative_error(
context.GetBaseEnthalpyTrue(), frozenEnthalpy, communicator
) == 0.0
);
CHECK(
gravity_prepared_test_utils::relative_error(
context.GetBaseGravityPotentialTrue(), frozenGravityPotential,
communicator
) == 0.0
);
CHECK(
gravity_prepared_test_utils::relative_error(
context.GetDisplacementTrue(), frozenDisplacement, communicator
) == 0.0
);
CHECK(context.GetBernoulliConstant() == frozenBernoulliConstant);
++dependencies.enthalpy.revision;
const auto enthalpyReport = context.Prepare(
hydrostatic_context_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
dependencies
);
hydrostatic_context_test_utils::check_base_only(enthalpyReport);
CHECK(enthalpyReport.updatedEnthalpy);
CHECK_FALSE(enthalpyReport.updatedGravityPotential);
CHECK_FALSE(enthalpyReport.updatedDisplacement);
CHECK_FALSE(enthalpyReport.updatedBernoulliConstant);
CHECK(context.GetBaseEnthalpyTrue()(0) == enthalpy(0));
++dependencies.gravityPotential.revision;
const auto gravityPotentialReport = context.Prepare(
hydrostatic_context_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
dependencies
);
hydrostatic_context_test_utils::check_base_only(gravityPotentialReport);
CHECK_FALSE(gravityPotentialReport.updatedEnthalpy);
CHECK(gravityPotentialReport.updatedGravityPotential);
CHECK_FALSE(gravityPotentialReport.updatedDisplacement);
CHECK_FALSE(gravityPotentialReport.updatedBernoulliConstant);
CHECK(context.GetBaseGravityPotentialTrue()(0) == gravityPotential(0));
++dependencies.bernoulliConstant.revision;
const auto bernoulliReport = context.Prepare(
hydrostatic_context_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
dependencies
);
hydrostatic_context_test_utils::check_base_only(bernoulliReport);
CHECK_FALSE(bernoulliReport.updatedEnthalpy);
CHECK_FALSE(bernoulliReport.updatedGravityPotential);
CHECK_FALSE(bernoulliReport.updatedDisplacement);
CHECK(bernoulliReport.updatedBernoulliConstant);
CHECK(context.GetBernoulliConstant() == bernoulliConstant);
++dependencies.rotation.revision;
const auto rotationReport = context.Prepare(
hydrostatic_context_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
dependencies
);
CHECK_FALSE(rotationReport.preparedStaticDependencies);
CHECK_FALSE(rotationReport.preparedGeometryState);
CHECK(rotationReport.preparedRotationDependencies);
CHECK(rotationReport.preparedBaseState);
CHECK_FALSE(rotationReport.updatedEnthalpy);
CHECK_FALSE(rotationReport.updatedGravityPotential);
CHECK_FALSE(rotationReport.updatedDisplacement);
CHECK_FALSE(rotationReport.updatedBernoulliConstant);
++dependencies.displacement.revision;
const auto displacementReport = context.Prepare(
hydrostatic_context_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
dependencies
);
CHECK_FALSE(displacementReport.preparedStaticDependencies);
CHECK(displacementReport.preparedGeometryState);
CHECK(displacementReport.preparedRotationDependencies);
CHECK(displacementReport.preparedBaseState);
CHECK_FALSE(displacementReport.updatedEnthalpy);
CHECK_FALSE(displacementReport.updatedGravityPotential);
CHECK(displacementReport.updatedDisplacement);
CHECK_FALSE(displacementReport.updatedBernoulliConstant);
CHECK(context.GetDisplacementTrue()(0) == displacement(0));
++dependencies.discretization.revision;
const auto discretizationReport = context.Prepare(
hydrostatic_context_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
dependencies
);
CHECK(discretizationReport.preparedStaticDependencies);
CHECK(discretizationReport.preparedGeometryState);
CHECK(discretizationReport.preparedRotationDependencies);
CHECK(discretizationReport.preparedBaseState);
CHECK(discretizationReport.updatedEnthalpy);
CHECK(discretizationReport.updatedGravityPotential);
CHECK(discretizationReport.updatedDisplacement);
CHECK(discretizationReport.updatedBernoulliConstant);
const auto finalStatistics = context.GetPreparationStatistics();
CHECK(finalStatistics.staticPreparations == 2);
CHECK(finalStatistics.geometryPreparations == 3);
CHECK(finalStatistics.rotationPreparations == 4);
CHECK(finalStatistics.baseStatePreparations == 7);
}
TEST_CASE(
"Hydrostatic Context Uses Identity In Every Dependency",
tags::barotrope &tags::contexts &tags::hydro &tags::prepared &tags::unit
) {
auto args = test_utils::setup_args();
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
mean_field::operators::context::hydrostatic::HydrostaticEquilibriumContext
context(f, *f.domainMapperStateless);
mfem::Vector enthalpy =
gravity_prepared_test_utils::make_deterministic_vector(
f.enthalpyFes->GetTrueVSize(), 0.23
);
const mfem::Vector gravityPotential =
gravity_prepared_test_utils::make_deterministic_vector(
f.gravityPotentialFes->GetTrueVSize(), 0.41
);
const mfem::Vector displacement =
gravity_prepared_test_utils::make_displacement(f, 0.60);
constexpr double bernoulliConstant = 0.81;
mean_field::operators::context::hydrostatic::
HydrostaticEquilibriumDependencies dependencies =
hydrostatic_context_test_utils::make_dependencies();
const auto preparedEnthalpyDependency = dependencies.enthalpy;
auto olderSameIdentity = preparedEnthalpyDependency;
--olderSameIdentity.revision;
auto resetNewIdentity = preparedEnthalpyDependency;
++resetNewIdentity.identity;
resetNewIdentity.revision = 0;
CHECK_FALSE(olderSameIdentity.CanFollow(preparedEnthalpyDependency));
CHECK(resetNewIdentity.CanFollow(preparedEnthalpyDependency));
context.Prepare(
hydrostatic_context_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
dependencies
);
const mfem::Vector firstFrozenEnthalpy = context.GetBaseEnthalpyTrue();
enthalpy(0) += 0.33;
const auto sameStampReport = context.Prepare(
hydrostatic_context_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
dependencies
);
CHECK_FALSE(sameStampReport.DidAnyWork());
CHECK(context.GetBaseEnthalpyTrue()(0) == firstFrozenEnthalpy(0));
++dependencies.enthalpy.identity;
dependencies.enthalpy.revision = 0;
const auto newIdentityReport = context.Prepare(
hydrostatic_context_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
dependencies
);
hydrostatic_context_test_utils::check_base_only(newIdentityReport);
CHECK(newIdentityReport.updatedEnthalpy);
CHECK(context.GetBaseEnthalpyTrue()(0) == enthalpy(0));
CHECK(context.MatchesDependencies(dependencies));
++dependencies.rotation.identity;
dependencies.rotation.revision = 0;
const auto newRotationIdentityReport = context.Prepare(
hydrostatic_context_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
dependencies
);
CHECK_FALSE(newRotationIdentityReport.preparedStaticDependencies);
CHECK_FALSE(newRotationIdentityReport.preparedGeometryState);
CHECK(newRotationIdentityReport.preparedRotationDependencies);
CHECK(newRotationIdentityReport.preparedBaseState);
const auto statistics = context.GetPreparationStatistics();
CHECK(statistics.staticPreparations == 1);
CHECK(statistics.geometryPreparations == 1);
CHECK(statistics.rotationPreparations == 2);
CHECK(statistics.baseStatePreparations == 3);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,628 @@
#include <memory>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <mfem.hpp>
import mean_field;
import test_helpers;
namespace {
mfem::Vector make_zero_displacement(const mean_field::fem::FEM &f) {
mfem::Vector displacement(f.displacementFes->GetTrueVSize());
displacement = 0.0;
return displacement;
}
mfem::Vector project_constant(
mfem::ParFiniteElementSpace &finiteElementSpace,
const double value
) {
mfem::ConstantCoefficient coefficient(value);
mfem::ParGridFunction field(&finiteElementSpace);
field.ProjectCoefficient(coefficient);
mfem::Vector trueVector;
field.GetTrueDofs(trueVector);
return trueVector;
}
namespace barotropic_closure_geometry_test_utils {
mfem::Vector project_scalar_field(
mfem::ParFiniteElementSpace &finiteElementSpace,
mfem::Coefficient &coefficient
) {
mfem::ParGridFunction field(&finiteElementSpace);
field.ProjectCoefficient(coefficient);
mfem::Vector trueVector;
field.GetTrueDofs(trueVector);
return trueVector;
}
mfem::Vector make_base_density(const mean_field::fem::FEM &f) {
mfem::FunctionCoefficient coefficient(
[](const mfem::Vector &position) {
return 0.55 + 0.025 * position(0) - 0.010 * position(1) +
0.006 * position(2);
}
);
return project_scalar_field(*f.densityFes, coefficient);
}
mfem::Vector make_base_enthalpy(const mean_field::fem::FEM &f) {
mfem::FunctionCoefficient coefficient(
[](const mfem::Vector &position) {
return 0.90 + 0.020 * position(0) - 0.010 * position(1) +
0.005 * position(2);
}
);
return project_scalar_field(*f.enthalpyFes, coefficient);
}
} // namespace barotropic_closure_geometry_test_utils
} // namespace
TEST_CASE(
"Barotropic Closure Vanishes For A Representable Constant State",
tags::hydro &tags::residuals &tags::unit &tags::closure &tags::kernels
&tags::barotrope
) {
auto args = test_utils::setup_args();
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
const mean_field::physics::PolytropicBarotrope barotrope(3.0, 1.5);
constexpr double enthalpyValue = 0.8;
const double densityValue = barotrope.density_from_enthalpy(enthalpyValue);
const mfem::Vector enthalpy =
project_constant(*f.enthalpyFes, enthalpyValue);
const mfem::Vector density = project_constant(*f.densityFes, densityValue);
const mfem::Vector displacement = make_zero_displacement(f);
mfem::Vector residual;
mfem::Vector scale;
mean_field::operators::kernels::apply_barotropic_closure(
f, *f.domainMapperStateless, barotrope, density, enthalpy, displacement,
residual
);
mean_field::operators::kernels::apply_barotropic_closure_density_action(
f, *f.domainMapperStateless, barotrope, density, displacement, scale
);
const MPI_Comm communicator = f.mesh->GetComm();
const double relativeResidual =
gravity_prepared_test_utils::global_norm(residual, communicator) /
gravity_prepared_test_utils::global_norm(scale, communicator);
INFO("Relative constant-state closure residual = " << relativeResidual);
CHECK(relativeResidual < 5.0e-12);
}
TEST_CASE(
"Barotropic Closure Density Action Matches The Stellar Mass Matrix",
tags::hydro &tags::jacobian &tags::unit &tags::closure &tags::kernels
&tags::barotrope
) {
auto args = test_utils::setup_args();
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
const mean_field::physics::PolytropicBarotrope barotrope(3.0, 1.5);
const mfem::Vector displacement = make_zero_displacement(f);
const mfem::Vector densityVariation =
gravity_prepared_test_utils::make_deterministic_vector(
f.densityFes->GetTrueVSize(), 0.37
);
mfem::Vector kernelAction;
mean_field::operators::kernels::apply_barotropic_closure_density_action(
f, *f.domainMapperStateless, barotrope, densityVariation, displacement,
kernelAction
);
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;
}
}
mfem::ParBilinearForm massForm(f.densityFes.get());
massForm.AddDomainIntegrator(new mfem::MassIntegrator(), stellarMarker);
massForm.Assemble();
massForm.Finalize();
std::unique_ptr<mfem::HypreParMatrix> massMatrix(
massForm.ParallelAssemble()
);
REQUIRE(massMatrix != nullptr);
REQUIRE(massMatrix->Width() == densityVariation.Size());
mfem::Vector referenceAction(massMatrix->Height());
referenceAction = 0.0;
massMatrix->Mult(densityVariation, referenceAction);
const double relativeError = gravity_prepared_test_utils::relative_error(
kernelAction, referenceAction, f.mesh->GetComm()
);
INFO("Density-action mass-matrix error = " << relativeError);
CHECK(relativeError < 5.0e-12);
}
TEST_CASE(
"Barotropic Closure Jacobian Matches A Combined Centered Difference",
tags::hydro &tags::jacobian &tags::unit &tags::closure &tags::kernels
&tags::barotrope
) {
auto args = test_utils::setup_args();
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
const mean_field::physics::PolytropicBarotrope barotrope(3.0, 1.5);
mfem::FunctionCoefficient densityCoefficient(
[](const mfem::Vector &position) {
return 0.4 + 0.03 * position(0) - 0.01 * position(1);
}
);
mfem::FunctionCoefficient enthalpyCoefficient(
[](const mfem::Vector &position) {
return 0.9 + 0.02 * position(0) - 0.01 * position(1);
}
);
mfem::FunctionCoefficient enthalpyVariationCoefficient(
[](const mfem::Vector &position) {
return 0.07 + 0.015 * position(0) + 0.008 * position(2);
}
);
mfem::ParGridFunction densityField(f.densityFes.get());
mfem::ParGridFunction enthalpyField(f.enthalpyFes.get());
mfem::ParGridFunction enthalpyVariationField(f.enthalpyFes.get());
densityField.ProjectCoefficient(densityCoefficient);
enthalpyField.ProjectCoefficient(enthalpyCoefficient);
enthalpyVariationField.ProjectCoefficient(enthalpyVariationCoefficient);
mfem::Vector density;
mfem::Vector enthalpy;
mfem::Vector enthalpyVariation;
densityField.GetTrueDofs(density);
enthalpyField.GetTrueDofs(enthalpy);
enthalpyVariationField.GetTrueDofs(enthalpyVariation);
const mfem::Vector densityVariation =
gravity_prepared_test_utils::make_deterministic_vector(
f.densityFes->GetTrueVSize(), 0.63
);
const mfem::Vector displacement =
gravity_prepared_test_utils::make_displacement(f, 1.0);
constexpr double differenceStep = 1.0e-6;
const mfem::Vector plusDensity =
gravity_prepared_test_utils::linear_combination(
density, 1.0, densityVariation, differenceStep
);
const mfem::Vector minusDensity =
gravity_prepared_test_utils::linear_combination(
density, 1.0, densityVariation, -differenceStep
);
const mfem::Vector plusEnthalpy =
gravity_prepared_test_utils::linear_combination(
enthalpy, 1.0, enthalpyVariation, differenceStep
);
const mfem::Vector minusEnthalpy =
gravity_prepared_test_utils::linear_combination(
enthalpy, 1.0, enthalpyVariation, -differenceStep
);
mfem::Vector plusResidual;
mfem::Vector minusResidual;
mean_field::operators::kernels::apply_barotropic_closure(
f, *f.domainMapperStateless, barotrope, plusDensity, plusEnthalpy,
displacement, plusResidual
);
mean_field::operators::kernels::apply_barotropic_closure(
f, *f.domainMapperStateless, barotrope, minusDensity, minusEnthalpy,
displacement, minusResidual
);
mfem::Vector finiteDifference(plusResidual);
finiteDifference -= minusResidual;
finiteDifference *= 1.0 / (2.0 * differenceStep);
mfem::Vector densityAction;
mfem::Vector enthalpyAction;
mean_field::operators::kernels::apply_barotropic_closure_density_action(
f, *f.domainMapperStateless, barotrope, densityVariation, displacement,
densityAction
);
mean_field::operators::kernels::apply_barotropic_closure_enthalpy_action(
f, *f.domainMapperStateless, barotrope, enthalpy, enthalpyVariation,
displacement, enthalpyAction
);
mfem::Vector analyticAction(densityAction);
analyticAction += enthalpyAction;
const double relativeError = gravity_prepared_test_utils::relative_error(
analyticAction, finiteDifference, f.mesh->GetComm()
);
INFO("Combined EOS Jacobian error = " << relativeError);
CHECK(relativeError < 2.0e-8);
}
TEST_CASE(
"Barotropic Closure Density Action Excludes Vacuum And Uses Mapped Volume",
tags::hydro &tags::mapping &tags::unit &tags::closure &tags::barotrope
&tags::kernels
) {
auto args = test_utils::setup_args();
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
const mean_field::physics::PolytropicBarotrope barotrope(3.0, 1.5);
const mfem::Vector stellarDensity =
gravity_prepared_test_utils::make_domain_supported_density(f, true);
const mfem::Vector vacuumDensity =
gravity_prepared_test_utils::make_domain_supported_density(f, false);
const mfem::Vector identityDisplacement =
gravity_prepared_test_utils::make_displacement(f, 0.0);
const mfem::Vector deformedDisplacement =
gravity_prepared_test_utils::make_displacement(f, 1.0);
mfem::Vector stellarAction;
mfem::Vector vacuumAction;
mfem::Vector deformedAction;
mean_field::operators::kernels::apply_barotropic_closure_density_action(
f, *f.domainMapperStateless, barotrope, stellarDensity,
identityDisplacement, stellarAction
);
mean_field::operators::kernels::apply_barotropic_closure_density_action(
f, *f.domainMapperStateless, barotrope, vacuumDensity,
identityDisplacement, vacuumAction
);
mean_field::operators::kernels::apply_barotropic_closure_density_action(
f, *f.domainMapperStateless, barotrope, stellarDensity,
deformedDisplacement, deformedAction
);
const MPI_Comm communicator = f.mesh->GetComm();
const double stellarNorm =
gravity_prepared_test_utils::global_norm(stellarAction, communicator);
const double vacuumNorm =
gravity_prepared_test_utils::global_norm(vacuumAction, communicator);
const double geometryChange = gravity_prepared_test_utils::relative_error(
deformedAction, stellarAction, communicator
);
INFO("Stellar action norm = " << stellarNorm);
INFO("Vacuum action norm = " << vacuumNorm);
INFO("Relative mapped-volume change = " << geometryChange);
CHECK(stellarNorm > 0.0);
CHECK(vacuumNorm <= 1.0e-13 * stellarNorm);
CHECK(geometryChange > 1.0e-5);
}
TEST_CASE(
"Barotropic Closure Displacement Action Matches Centered Differences",
tags::barotrope &tags::closure &tags::hydro &tags::integration
&tags::jacobian &tags::mapping &tags::physics
) {
auto args = test_utils::setup_args();
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.domainMapperStateless != nullptr);
const mean_field::physics::PolytropicBarotrope barotrope(3.0, 1.5);
const mfem::Vector baseDensity =
barotropic_closure_geometry_test_utils::make_base_density(f);
const mfem::Vector baseEnthalpy =
barotropic_closure_geometry_test_utils::make_base_enthalpy(f);
const mfem::Vector displacementVariation =
gravity_prepared_test_utils::make_displacement(f, 0.65);
constexpr double differenceStep = 1.0e-5;
const MPI_Comm communicator = f.mesh->GetComm();
for (const double deformationScale : {0.0, 1.0}) {
DYNAMIC_SECTION("Base deformation scale = " << deformationScale) {
const mfem::Vector baseDisplacement =
gravity_prepared_test_utils::make_displacement(
f, deformationScale
);
mfem::Vector plusDisplacement(baseDisplacement);
mfem::Vector minusDisplacement(baseDisplacement);
plusDisplacement.Add(differenceStep, displacementVariation);
minusDisplacement.Add(-differenceStep, displacementVariation);
mfem::Vector plusResidual;
mfem::Vector minusResidual;
mfem::Vector analyticAction;
mean_field::operators::kernels::apply_barotropic_closure(
f, *f.domainMapperStateless, barotrope, baseDensity,
baseEnthalpy, plusDisplacement, plusResidual
);
mean_field::operators::kernels::apply_barotropic_closure(
f, *f.domainMapperStateless, barotrope, baseDensity,
baseEnthalpy, minusDisplacement, minusResidual
);
mean_field::operators::kernels::
apply_barotropic_closure_displacement_action(
f, *f.domainMapperStateless, barotrope, baseDensity,
baseEnthalpy, baseDisplacement, displacementVariation,
analyticAction
);
mfem::Vector finiteDifference(plusResidual);
finiteDifference -= minusResidual;
finiteDifference *= 1.0 / (2.0 * differenceStep);
const double analyticNorm =
gravity_prepared_test_utils::global_norm(
analyticAction, communicator
);
const double finiteDifferenceNorm =
gravity_prepared_test_utils::global_norm(
finiteDifference, communicator
);
const double relativeError =
gravity_prepared_test_utils::relative_error(
analyticAction, finiteDifference, communicator
);
INFO("Base deformation scale = " << deformationScale);
INFO("Analytic geometry-action norm = " << analyticNorm);
INFO(
"Finite-difference geometry-action norm = "
<< finiteDifferenceNorm
);
INFO("Geometry-action relative error = " << relativeError);
REQUIRE(analyticNorm > 1.0e-12);
REQUIRE(finiteDifferenceNorm > 1.0e-12);
CHECK(relativeError < 5.0e-8);
}
}
}
TEST_CASE(
"Barotropic Closure Displacement Action Is Linear In Its Direction",
tags::barotrope &tags::closure &tags::hydro &tags::jacobian &tags::mapping
&tags::physics &tags::unit
) {
auto args = test_utils::setup_args();
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.domainMapperStateless != nullptr);
const mean_field::physics::PolytropicBarotrope barotrope(3.0, 1.5);
const mfem::Vector baseDensity =
barotropic_closure_geometry_test_utils::make_base_density(f);
const mfem::Vector baseEnthalpy =
barotropic_closure_geometry_test_utils::make_base_enthalpy(f);
const mfem::Vector baseDisplacement =
gravity_prepared_test_utils::make_displacement(f, 0.8);
const mfem::Vector firstDirection =
gravity_prepared_test_utils::make_displacement(f, 0.4);
mfem::Vector secondDirection =
gravity_prepared_test_utils::make_deterministic_vector(
f.displacementFes->GetTrueVSize(), 0.91
);
secondDirection *= 0.01;
constexpr double firstScale = 1.7;
constexpr double secondScale = -0.43;
mfem::Vector combinedDirection(firstDirection);
combinedDirection *= firstScale;
combinedDirection.Add(secondScale, secondDirection);
mfem::Vector zeroDirection(f.displacementFes->GetTrueVSize());
zeroDirection = 0.0;
mfem::Vector firstAction;
mfem::Vector secondAction;
mfem::Vector combinedAction;
mfem::Vector zeroAction;
mean_field::operators::kernels::
apply_barotropic_closure_displacement_action(
f, *f.domainMapperStateless, barotrope, baseDensity, baseEnthalpy,
baseDisplacement, firstDirection, firstAction
);
mean_field::operators::kernels::
apply_barotropic_closure_displacement_action(
f, *f.domainMapperStateless, barotrope, baseDensity, baseEnthalpy,
baseDisplacement, secondDirection, secondAction
);
mean_field::operators::kernels::
apply_barotropic_closure_displacement_action(
f, *f.domainMapperStateless, barotrope, baseDensity, baseEnthalpy,
baseDisplacement, combinedDirection, combinedAction
);
mean_field::operators::kernels::
apply_barotropic_closure_displacement_action(
f, *f.domainMapperStateless, barotrope, baseDensity, baseEnthalpy,
baseDisplacement, zeroDirection, zeroAction
);
mfem::Vector expectedAction(firstAction);
expectedAction *= firstScale;
expectedAction.Add(secondScale, secondAction);
const MPI_Comm communicator = f.mesh->GetComm();
const double expectedNorm =
gravity_prepared_test_utils::global_norm(expectedAction, communicator);
const double linearityError = gravity_prepared_test_utils::relative_error(
combinedAction, expectedAction, communicator
);
const double zeroActionNorm =
gravity_prepared_test_utils::global_norm(zeroAction, communicator);
INFO("Expected combined-action norm = " << expectedNorm);
INFO("Directional-linearity error = " << linearityError);
INFO("Zero-direction action norm = " << zeroActionNorm);
REQUIRE(expectedNorm > 1.0e-12);
CHECK(linearityError < 5.0e-12);
CHECK(zeroActionNorm <= 5.0e-14 * expectedNorm);
}
TEST_CASE(
"Barotropic Closure Displacement Action Excludes Vacuum",
tags::barotrope &tags::closure &tags::hydro &tags::mapping &tags::physics
&tags::unit
) {
auto args = test_utils::setup_args();
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.domainMapperStateless != nullptr);
const mean_field::physics::PolytropicBarotrope barotrope(3.0, 1.5);
const mfem::Vector stellarDensity =
gravity_prepared_test_utils::make_domain_supported_density(f, true);
const mfem::Vector vacuumDensity =
gravity_prepared_test_utils::make_domain_supported_density(f, false);
mfem::Vector zeroEnthalpy(f.enthalpyFes->GetTrueVSize());
zeroEnthalpy = 0.0;
const mfem::Vector baseDisplacement =
gravity_prepared_test_utils::make_displacement(f, 0.7);
const mfem::Vector displacementVariation =
gravity_prepared_test_utils::make_displacement(f, 0.5);
mfem::Vector stellarAction;
mfem::Vector vacuumAction;
mean_field::operators::kernels::
apply_barotropic_closure_displacement_action(
f, *f.domainMapperStateless, barotrope, stellarDensity,
zeroEnthalpy, baseDisplacement, displacementVariation, stellarAction
);
mean_field::operators::kernels::
apply_barotropic_closure_displacement_action(
f, *f.domainMapperStateless, barotrope, vacuumDensity, zeroEnthalpy,
baseDisplacement, displacementVariation, vacuumAction
);
const MPI_Comm communicator = f.mesh->GetComm();
const double stellarNorm =
gravity_prepared_test_utils::global_norm(stellarAction, communicator);
const double vacuumNorm =
gravity_prepared_test_utils::global_norm(vacuumAction, communicator);
INFO("Stellar geometry-action norm = " << stellarNorm);
INFO("Vacuum geometry-action norm = " << vacuumNorm);
REQUIRE(stellarNorm > 1.0e-12);
CHECK(vacuumNorm <= 1.0e-13 * stellarNorm);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,424 @@
#include <cmath>
#include <limits>
#include <array>
#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);
mfem::Array<int> stellarElementMask;
mean_field::utils::populate_element_mask(
f.mesh.get(), mean_field::utils::DOMAINS::STELLAR,
stellarElementMask
);
mfem::Array<int> stellarEnthalpyTrueDofs;
mean_field::utils::populate_domain_tdofs(
f.enthalpyFes.get(), stellarElementMask, stellarEnthalpyTrueDofs
);
for (int listIndex = 0; listIndex < stellarEnthalpyTrueDofs.Size();
++listIndex) {
const int trueDof = stellarEnthalpyTrueDofs[listIndex];
MFEM_VERIFY(
trueDof >= 0 && trueDof < enthalpyTrue.Size(),
"The stellar enthalpy true-DOF mask contains an "
"invalid index."
);
enthalpyTrue(trueDof) = 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;
}
} // 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::physics::PolytropicBarotrope 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::physics::PolytropicBarotrope 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::physics::PolytropicBarotrope 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::physics::PolytropicBarotrope 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 Respects byNODES Component Layout",
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::physics::PolytropicBarotrope 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()
);
}
}
double meanDiagonalWork = 0.0;
for (int component = 0; component < dimension; ++component) {
meanDiagonalWork += virtualWork(component, component);
}
meanDiagonalWork /= static_cast<double>(dimension);
// INFO(
// "Affine pressure virtual-work tensor:\n"
// << virtualWork
// );
INFO("Mean diagonal virtual work = " << meanDiagonalWork);
REQUIRE(
std::abs(meanDiagonalWork) >
100.0 * std::numeric_limits<double>::epsilon()
);
const double comparisonTolerance = 1.0e-8 * std::abs(meanDiagonalWork);
for (int component = 0; component < dimension; ++component) {
for (int coordinate = 0; coordinate < dimension; ++coordinate) {
const double computedWork = virtualWork(component, coordinate);
CAPTURE(
component, coordinate, computedWork, meanDiagonalWork,
comparisonTolerance
);
if (component == coordinate) {
CHECK(
std::abs(computedWork - meanDiagonalWork) <=
comparisonTolerance
);
} else {
CHECK(std::abs(computedWork) <= comparisonTolerance);
}
}
}
}

View File

@@ -0,0 +1,995 @@
#include <algorithm>
#include <array>
#include <catch2/catch_test_macros.hpp>
#include <cstddef>
#include <cstdint>
#include <mfem.hpp>
import mean_field;
import test_helpers;
namespace {
mfem::Vector project_scalar(
mfem::ParFiniteElementSpace &finiteElementSpace,
mfem::Coefficient &coefficient
) {
mfem::ParGridFunction field(&finiteElementSpace);
field.ProjectCoefficient(coefficient);
mfem::Vector trueVector;
field.GetTrueDofs(trueVector);
return trueVector;
}
mfem::Vector make_base_density(const mean_field::fem::FEM &f) {
mfem::FunctionCoefficient coefficient([](const mfem::Vector &position) {
return 0.42 + 0.025 * position(0) - 0.012 * position(1) +
0.007 * position(2);
});
return project_scalar(*f.densityFes, coefficient);
}
mfem::Vector make_base_enthalpy(const mean_field::fem::FEM &f) {
mfem::FunctionCoefficient coefficient([](const mfem::Vector &position) {
return 0.92 + 0.018 * position(0) - 0.011 * position(1) +
0.006 * position(2);
});
return project_scalar(*f.enthalpyFes, coefficient);
}
mfem::Vector make_enthalpy_variation(const mean_field::fem::FEM &f) {
mfem::FunctionCoefficient coefficient([](const mfem::Vector &position) {
return 0.065 + 0.014 * position(0) + 0.009 * position(2);
});
return project_scalar(*f.enthalpyFes, coefficient);
}
mfem::Vector make_combined_variation(
const mfem::Vector &densityVariation,
const mfem::Vector &enthalpyVariation
) {
mfem::Vector combinedVariation(
densityVariation.Size() + enthalpyVariation.Size()
);
for (int densityDof = 0; densityDof < densityVariation.Size();
++densityDof) {
combinedVariation(densityDof) = densityVariation(densityDof);
}
for (int enthalpyDof = 0; enthalpyDof < enthalpyVariation.Size();
++enthalpyDof) {
combinedVariation(densityVariation.Size() + enthalpyDof) =
enthalpyVariation(enthalpyDof);
}
return combinedVariation;
}
struct ClosureCondition {
const char *name;
double polytropicIndex;
double polytropicConstant;
double enthalpyOffset;
double enthalpyGradient;
double densityFactor;
double densityOffset;
double densityGradient;
double deformationScale;
double directionPhase;
};
inline constexpr std::array<ClosureCondition, 3> conditions{
{{.name = "Linear barotrope on identity geometry",
.polytropicIndex = 1.0,
.polytropicConstant = 0.8,
.enthalpyOffset = 0.65,
.enthalpyGradient = 0.06,
.densityFactor = 0.80,
.densityOffset = 0.015,
.densityGradient = 0.004,
.deformationScale = 0.0,
.directionPhase = 0.31},
{.name = "Fractional barotrope on moderate deformation",
.polytropicIndex = 1.5,
.polytropicConstant = 1.2,
.enthalpyOffset = 0.90,
.enthalpyGradient = 0.09,
.densityFactor = 1.15,
.densityOffset = -0.003,
.densityGradient = 0.003,
.deformationScale = 0.45,
.directionPhase = 0.53},
{.name = "Target n=3 barotrope on strong deformation",
.polytropicIndex = 3.0,
.polytropicConstant = 1.5,
.enthalpyOffset = 1.20,
.enthalpyGradient = 0.12,
.densityFactor = 1.40,
.densityOffset = 0.006,
.densityGradient = 0.002,
.deformationScale = 1.0,
.directionPhase = 0.79}}
};
double evaluate_enthalpy(
const mfem::Vector &position,
const ClosureCondition &condition
) {
return condition.enthalpyOffset +
condition.enthalpyGradient *
(0.50 * position(0) - 0.30 * position(1) +
0.20 * position(2));
}
mfem::Vector project_scalar_field(
mfem::ParFiniteElementSpace &finiteElementSpace,
mfem::Coefficient &coefficient
) {
mfem::ParGridFunction field(&finiteElementSpace);
field.ProjectCoefficient(coefficient);
mfem::Vector trueVector;
field.GetTrueDofs(trueVector);
return trueVector;
}
mfem::Vector make_enthalpy(
const mean_field::fem::FEM &f,
const ClosureCondition &condition
) {
mfem::FunctionCoefficient coefficient(
[condition](const mfem::Vector &position) {
return evaluate_enthalpy(position, condition);
}
);
return project_scalar_field(*f.enthalpyFes, coefficient);
}
mfem::Vector make_density(
const mean_field::fem::FEM &f,
const mean_field::physics::PolytropicBarotrope &barotrope,
const ClosureCondition &condition
) {
mfem::FunctionCoefficient coefficient(
[&barotrope, condition](const mfem::Vector &position) {
const double enthalpy = evaluate_enthalpy(position, condition);
return condition.densityFactor *
barotrope.density_from_enthalpy(enthalpy) +
condition.densityOffset +
condition.densityGradient *
(0.40 * position(0) + 0.25 * position(1) -
0.15 * position(2));
}
);
return project_scalar_field(*f.densityFes, coefficient);
}
mfem::Vector make_constant_field(
mfem::ParFiniteElementSpace &finiteElementSpace,
const double value
) {
mfem::ConstantCoefficient coefficient(value);
return project_scalar_field(finiteElementSpace, coefficient);
}
} // namespace
TEST_CASE(
"Prepared Barotropic Closure Matches Stateless Kernels",
tags::hydro &tags::prepared &tags::unit
) {
auto args = test_utils::setup_args();
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
const mean_field::physics::PolytropicBarotrope barotrope(3.0, 1.5);
mean_field::operators::PreparedBarotropicClosureOperator preparedOperator(
f, *f.domainMapperStateless, barotrope
);
REQUIRE_FALSE(preparedOperator.IsPrepared());
REQUIRE(preparedOperator.Height() == f.densityFes->GetTrueVSize());
REQUIRE(
preparedOperator.Width() ==
f.densityFes->GetTrueVSize() + f.enthalpyFes->GetTrueVSize()
);
REQUIRE(preparedOperator.GetDensitySize() == f.densityFes->GetTrueVSize());
REQUIRE(
preparedOperator.GetEnthalpySize() == f.enthalpyFes->GetTrueVSize()
);
const mfem::Vector baseDensity = make_base_density(f);
const mfem::Vector baseEnthalpy = make_base_enthalpy(f);
const mfem::Vector displacement =
gravity_prepared_test_utils::make_displacement(f, 1.0);
const mfem::Vector densityVariation =
gravity_prepared_test_utils::make_deterministic_vector(
f.densityFes->GetTrueVSize(), 0.43
);
const mfem::Vector displacementVariation =
gravity_prepared_test_utils::make_displacement(f, 0.63);
const mfem::Vector enthalpyVariation = make_enthalpy_variation(f);
mfem::Vector zeroDensity(f.densityFes->GetTrueVSize());
zeroDensity = 0.0;
mfem::Vector zeroEnthalpy(f.enthalpyFes->GetTrueVSize());
zeroEnthalpy = 0.0;
preparedOperator.Prepare(baseDensity, baseEnthalpy, displacement);
mfem::Vector preparedResidual;
mfem::Vector preparedDensityAction;
mfem::Vector preparedEnthalpyAction;
mfem::Vector preparedSplitAction;
mfem::Vector preparedCombinedAction;
preparedOperator.BuildResidual(preparedResidual);
preparedOperator.Mult(
densityVariation, zeroEnthalpy, displacementVariation,
preparedDensityAction
);
preparedOperator.Mult(
zeroDensity, enthalpyVariation, displacementVariation,
preparedEnthalpyAction
);
preparedOperator.Mult(
densityVariation, enthalpyVariation, displacementVariation,
preparedSplitAction
);
const mfem::Vector combinedVariation =
make_combined_variation(densityVariation, enthalpyVariation);
preparedOperator.Mult(combinedVariation, preparedCombinedAction);
mfem::Vector referenceResidual;
mfem::Vector referenceDensityAction;
mfem::Vector referenceEnthalpyAction;
mfem::Vector referenceDisplacementAction;
mean_field::operators::kernels::apply_barotropic_closure(
f, *f.domainMapperStateless, barotrope, baseDensity, baseEnthalpy,
displacement, referenceResidual
);
mean_field::operators::kernels::apply_barotropic_closure_density_action(
f, *f.domainMapperStateless, barotrope, densityVariation, displacement,
referenceDensityAction
);
mean_field::operators::kernels::apply_barotropic_closure_enthalpy_action(
f, *f.domainMapperStateless, barotrope, baseEnthalpy, enthalpyVariation,
displacement, referenceEnthalpyAction
);
mean_field::operators::kernels::
apply_barotropic_closure_displacement_action(
f, *f.domainMapperStateless, barotrope, baseDensity, baseEnthalpy,
displacement, displacementVariation, referenceDisplacementAction
);
mfem::Vector referenceCombinedAction(referenceDensityAction);
referenceCombinedAction += referenceEnthalpyAction;
referenceCombinedAction += referenceDisplacementAction;
const MPI_Comm communicator = f.mesh->GetComm();
const double residualError = gravity_prepared_test_utils::relative_error(
preparedResidual, referenceResidual, communicator
);
const double densityError = gravity_prepared_test_utils::relative_error(
preparedDensityAction, referenceDensityAction, communicator
);
const double enthalpyError = gravity_prepared_test_utils::relative_error(
preparedEnthalpyAction, referenceEnthalpyAction, communicator
);
const double splitError = gravity_prepared_test_utils::relative_error(
preparedSplitAction, referenceCombinedAction, communicator
);
const double combinedError = gravity_prepared_test_utils::relative_error(
preparedCombinedAction, referenceCombinedAction, communicator
);
INFO("Prepared residual error = " << residualError);
INFO("Prepared density-action error = " << densityError);
INFO("Prepared enthalpy-action error = " << enthalpyError);
INFO("Prepared split-action error = " << splitError);
INFO("Prepared combined-action error = " << combinedError);
CHECK(preparedOperator.IsPrepared());
CHECK(preparedOperator.GetPreparationCount() == 1);
CHECK(residualError < 2.0e-12);
CHECK(densityError < 2.0e-12);
CHECK(enthalpyError < 2.0e-12);
CHECK(splitError < 2.0e-12);
CHECK(combinedError < 2.0e-12);
}
TEST_CASE(
"Prepared Barotropic Closure Jacobian Matches Centered Difference",
tags::hydro &tags::jacobian &tags::prepared &tags::unit
) {
auto args = test_utils::setup_args();
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
const mean_field::physics::PolytropicBarotrope barotrope(3.0, 1.5);
mfem::Vector baseDensity = make_base_density(f);
mfem::Vector baseEnthalpy = make_base_enthalpy(f);
const mfem::Vector displacement =
gravity_prepared_test_utils::make_displacement(f, 1.0);
const mfem::Vector densityVariation =
gravity_prepared_test_utils::make_deterministic_vector(
f.densityFes->GetTrueVSize(), 0.71
);
const mfem::Vector displacementVariation =
gravity_prepared_test_utils::make_displacement(f, 0.63);
const mfem::Vector enthalpyVariation = make_enthalpy_variation(f);
mean_field::operators::PreparedBarotropicClosureOperator preparedOperator(
f, *f.domainMapperStateless, barotrope
);
preparedOperator.Prepare(baseDensity, baseEnthalpy, displacement);
mfem::Vector analyticAction;
preparedOperator.Mult(
densityVariation, enthalpyVariation, displacementVariation,
analyticAction
);
constexpr double differenceStep = 1.0e-6;
const mfem::Vector plusDensity =
gravity_prepared_test_utils::linear_combination(
baseDensity, 1.0, densityVariation, differenceStep
);
const mfem::Vector minusDensity =
gravity_prepared_test_utils::linear_combination(
baseDensity, 1.0, densityVariation, -differenceStep
);
const mfem::Vector plusEnthalpy =
gravity_prepared_test_utils::linear_combination(
baseEnthalpy, 1.0, enthalpyVariation, differenceStep
);
const mfem::Vector minusEnthalpy =
gravity_prepared_test_utils::linear_combination(
baseEnthalpy, 1.0, enthalpyVariation, -differenceStep
);
mfem::Vector plusResidual;
mfem::Vector minusResidual;
mean_field::operators::kernels::apply_barotropic_closure(
f, *f.domainMapperStateless, barotrope, plusDensity, plusEnthalpy,
displacement, plusResidual
);
mean_field::operators::kernels::apply_barotropic_closure(
f, *f.domainMapperStateless, barotrope, minusDensity, minusEnthalpy,
displacement, minusResidual
);
mfem::Vector finiteDifference(plusResidual);
finiteDifference -= minusResidual;
finiteDifference *= 1.0 / (2.0 * differenceStep);
const double relativeError = gravity_prepared_test_utils::relative_error(
analyticAction, finiteDifference, f.mesh->GetComm()
);
INFO("Prepared EOS centered-difference error = " << relativeError);
CHECK(relativeError < 2.0e-8);
}
TEST_CASE(
"Prepared Barotropic Closure Reuses Frozen Data",
tags::hydro &tags::prepared &tags::unit
) {
auto args = test_utils::setup_args();
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
const mean_field::physics::PolytropicBarotrope barotrope(3.0, 1.5);
mean_field::operators::PreparedBarotropicClosureOperator preparedOperator(
f, *f.domainMapperStateless, barotrope
);
CHECK_FALSE(preparedOperator.IsPrepared());
CHECK(preparedOperator.GetPreparationCount() == 0);
mfem::Vector baseDensity = make_base_density(f);
mfem::Vector baseEnthalpy = make_base_enthalpy(f);
mfem::Vector displacement =
gravity_prepared_test_utils::make_displacement(f, 1.0);
const mfem::Vector displacementVariation =
gravity_prepared_test_utils::make_displacement(f, 0.63);
const mfem::Vector densityVariation =
gravity_prepared_test_utils::make_deterministic_vector(
f.densityFes->GetTrueVSize(), 0.31
);
const mfem::Vector enthalpyVariation = make_enthalpy_variation(f);
preparedOperator.Prepare(baseDensity, baseEnthalpy, displacement);
REQUIRE(preparedOperator.IsPrepared());
REQUIRE(preparedOperator.GetPreparationCount() == 1);
mfem::Vector firstResidual;
mfem::Vector firstAction;
preparedOperator.BuildResidual(firstResidual);
preparedOperator.Mult(
densityVariation, enthalpyVariation, displacementVariation, firstAction
);
baseDensity = 7.0;
baseEnthalpy = 3.0;
displacement *= -4.0;
const std::uint64_t preparationCount =
preparedOperator.GetPreparationCount();
mfem::Vector repeatedResidual;
mfem::Vector repeatedAction;
preparedOperator.BuildResidual(repeatedResidual);
preparedOperator.Mult(
densityVariation, enthalpyVariation, displacementVariation,
repeatedAction
);
const MPI_Comm communicator = f.mesh->GetComm();
const double residualReuseError =
gravity_prepared_test_utils::relative_error(
repeatedResidual, firstResidual, communicator
);
const double actionReuseError = gravity_prepared_test_utils::relative_error(
repeatedAction, firstAction, communicator
);
INFO("Frozen residual reuse error = " << residualReuseError);
INFO("Frozen action reuse error = " << actionReuseError);
CHECK(residualReuseError < 2.0e-14);
CHECK(actionReuseError < 2.0e-14);
CHECK(preparedOperator.GetPreparationCount() == preparationCount);
}
TEST_CASE(
"Prepared Barotropic Closure Reprepares For New Geometry",
tags::hydro &tags::mapping &tags::prepared &tags::unit
) {
auto args = test_utils::setup_args();
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
const mean_field::physics::PolytropicBarotrope barotrope(3.0, 1.5);
const mfem::Vector baseDensity = make_base_density(f);
const mfem::Vector baseEnthalpy = make_base_enthalpy(f);
const mfem::Vector densityVariation =
gravity_prepared_test_utils::make_deterministic_vector(
f.densityFes->GetTrueVSize(), 0.59
);
mfem::Vector zeroEnthalpy(f.enthalpyFes->GetTrueVSize());
zeroEnthalpy = 0.0;
mean_field::operators::PreparedBarotropicClosureOperator preparedOperator(
f, *f.domainMapperStateless, barotrope
);
mfem::Vector identityAction;
mfem::Vector deformedAction;
for (const double deformationScale : {0.0, 1.0}) {
const mfem::Vector displacement =
gravity_prepared_test_utils::make_displacement(f, deformationScale);
const mfem::Vector displacementVariation =
gravity_prepared_test_utils::make_displacement(f, 0.63);
preparedOperator.Prepare(baseDensity, baseEnthalpy, displacement);
mfem::Vector preparedResidual;
mfem::Vector preparedAction;
mfem::Vector referenceResidual;
mfem::Vector referenceAction;
preparedOperator.BuildResidual(preparedResidual);
preparedOperator.Mult(
densityVariation, zeroEnthalpy, displacementVariation,
preparedAction
);
mean_field::operators::kernels::apply_barotropic_closure(
f, *f.domainMapperStateless, barotrope, baseDensity, baseEnthalpy,
displacement, referenceResidual
);
mean_field::operators::kernels::apply_barotropic_closure_density_action(
f, *f.domainMapperStateless, barotrope, densityVariation,
displacement, referenceAction
);
const MPI_Comm communicator = f.mesh->GetComm();
const double residualError =
gravity_prepared_test_utils::relative_error(
preparedResidual, referenceResidual, communicator
);
const double actionError = gravity_prepared_test_utils::relative_error(
preparedAction, referenceAction, communicator
);
INFO("Deformation scale = " << deformationScale);
INFO("Reprepared residual error = " << residualError);
INFO("Reprepared action error = " << actionError);
CHECK(residualError < 2.0e-12);
CHECK(actionError < 2.0e-12);
if (deformationScale == 0.0) {
identityAction = preparedAction;
} else {
deformedAction = preparedAction;
}
}
const double geometryChange = gravity_prepared_test_utils::relative_error(
deformedAction, identityAction, f.mesh->GetComm()
);
INFO("Prepared closure geometry change = " << geometryChange);
CHECK(preparedOperator.GetPreparationCount() == 2);
CHECK(geometryChange > 1.0e-5);
}
TEST_CASE(
"Complete Barotropic Closure Matches Blocks And Centered Differences "
"Across Conditions",
tags::barotrope &tags::closure &tags::hydro &tags::integration
&tags::jacobian &tags::mapping &tags::physics &tags::prepared
) {
auto args = test_utils::setup_args();
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.domainMapperStateless != nullptr);
const MPI_Comm communicator = f.mesh->GetComm();
constexpr double differenceStep = 1.0e-5;
for (std::size_t conditionIndex = 0; conditionIndex <
conditions.size();
++conditionIndex) {
const auto &condition =
conditions[conditionIndex];
DYNAMIC_SECTION(condition.name) {
const mean_field::physics::PolytropicBarotrope barotrope(
condition.polytropicIndex, condition.polytropicConstant
);
const mfem::Vector baseDensity =
make_density(f, barotrope, condition);
const mfem::Vector baseEnthalpy =
make_enthalpy(f, condition);
const mfem::Vector baseDisplacement =
gravity_prepared_test_utils::make_displacement(
f, condition.deformationScale
);
mfem::Vector densityVariation =
gravity_prepared_test_utils::make_deterministic_vector(
f.densityFes->GetTrueVSize(), condition.directionPhase
);
mfem::Vector enthalpyVariation =
gravity_prepared_test_utils::make_deterministic_vector(
f.enthalpyFes->GetTrueVSize(),
condition.directionPhase + 0.27
);
mfem::Vector displacementVariation =
gravity_prepared_test_utils::make_displacement(
f, condition.directionPhase
);
mfem::Vector densityAction;
mfem::Vector enthalpyAction;
mfem::Vector displacementAction;
mean_field::operators::kernels::
apply_barotropic_closure_density_action(
f, *f.domainMapperStateless, barotrope, densityVariation,
baseDisplacement, densityAction
);
mean_field::operators::kernels::
apply_barotropic_closure_enthalpy_action(
f, *f.domainMapperStateless, barotrope, baseEnthalpy,
enthalpyVariation, baseDisplacement, enthalpyAction
);
mean_field::operators::kernels::
apply_barotropic_closure_displacement_action(
f, *f.domainMapperStateless, barotrope, baseDensity,
baseEnthalpy, baseDisplacement, displacementVariation,
displacementAction
);
double densityActionNorm = gravity_prepared_test_utils::global_norm(
densityAction, communicator
);
double enthalpyActionNorm =
gravity_prepared_test_utils::global_norm(
enthalpyAction, communicator
);
double displacementActionNorm =
gravity_prepared_test_utils::global_norm(
displacementAction, communicator
);
REQUIRE(densityActionNorm > 1.0e-12);
REQUIRE(enthalpyActionNorm > 1.0e-12);
REQUIRE(displacementActionNorm > 1.0e-12);
const double targetActionNorm = std::min(
{densityActionNorm, enthalpyActionNorm, displacementActionNorm}
);
const double densityScale = targetActionNorm / densityActionNorm;
const double enthalpyScale = targetActionNorm / enthalpyActionNorm;
const double displacementScale =
targetActionNorm / displacementActionNorm;
densityVariation *= densityScale;
densityAction *= densityScale;
enthalpyVariation *= enthalpyScale;
enthalpyAction *= enthalpyScale;
displacementVariation *= displacementScale;
displacementAction *= displacementScale;
densityActionNorm = gravity_prepared_test_utils::global_norm(
densityAction, communicator
);
enthalpyActionNorm = gravity_prepared_test_utils::global_norm(
enthalpyAction, communicator
);
displacementActionNorm = gravity_prepared_test_utils::global_norm(
displacementAction, communicator
);
mean_field::operators::context::barotropic::
BarotropicClosureLinearizationContext context(
f, *f.domainMapperStateless, barotrope
);
const std::uint64_t revisionBase =
100 + static_cast<std::uint64_t>(10 * conditionIndex);
const mean_field::operators::context::barotropic::
BarotropicClosureRevisions revisions{
.density = revisionBase + 1,
.enthalpy = revisionBase + 2,
.displacement = revisionBase + 3
};
context.Prepare(
baseDensity, baseEnthalpy, baseDisplacement, revisions
);
mfem::Vector preparedResidual;
mfem::Vector referenceResidual;
context.BuildResidual(preparedResidual);
mean_field::operators::kernels::apply_barotropic_closure(
f, *f.domainMapperStateless, barotrope, baseDensity,
baseEnthalpy, baseDisplacement, referenceResidual
);
const double residualEvaluationError =
gravity_prepared_test_utils::relative_error(
preparedResidual, referenceResidual, communicator
);
mfem::Vector preparedAction;
context.GetOperator().Mult(
densityVariation, enthalpyVariation, displacementVariation,
preparedAction
);
mfem::Vector blockSum(densityAction);
blockSum += enthalpyAction;
blockSum += displacementAction;
const double blockAssemblyError =
gravity_prepared_test_utils::relative_error(
preparedAction, blockSum, communicator
);
mfem::Vector plusDensity(baseDensity);
mfem::Vector minusDensity(baseDensity);
mfem::Vector plusEnthalpy(baseEnthalpy);
mfem::Vector minusEnthalpy(baseEnthalpy);
mfem::Vector plusDisplacement(baseDisplacement);
mfem::Vector minusDisplacement(baseDisplacement);
plusDensity.Add(differenceStep, densityVariation);
minusDensity.Add(-differenceStep, densityVariation);
plusEnthalpy.Add(differenceStep, enthalpyVariation);
minusEnthalpy.Add(-differenceStep, enthalpyVariation);
plusDisplacement.Add(differenceStep, displacementVariation);
minusDisplacement.Add(-differenceStep, displacementVariation);
mfem::Vector plusResidual;
mfem::Vector minusResidual;
mean_field::operators::kernels::apply_barotropic_closure(
f, *f.domainMapperStateless, barotrope, plusDensity,
plusEnthalpy, plusDisplacement, plusResidual
);
mean_field::operators::kernels::apply_barotropic_closure(
f, *f.domainMapperStateless, barotrope, minusDensity,
minusEnthalpy, minusDisplacement, minusResidual
);
mfem::Vector finiteDifference(plusResidual);
finiteDifference -= minusResidual;
finiteDifference *= 1.0 / (2.0 * differenceStep);
mfem::Vector finiteDifferenceError(preparedAction);
finiteDifferenceError -= finiteDifference;
const double finiteDifferenceErrorNorm =
gravity_prepared_test_utils::global_norm(
finiteDifferenceError, communicator
);
const double blockNormSum =
densityActionNorm + enthalpyActionNorm + displacementActionNorm;
const double blockScaledDifferenceError =
finiteDifferenceErrorNorm / blockNormSum;
const double completeRelativeError =
gravity_prepared_test_utils::relative_error(
preparedAction, finiteDifference, communicator
);
INFO("Condition = " << condition.name);
INFO("Polytropic index = " << condition.polytropicIndex);
INFO("Deformation scale = " << condition.deformationScale);
INFO("Prepared residual error = " << residualEvaluationError);
INFO("Complete block-assembly error = " << blockAssemblyError);
INFO("Density-action norm = " << densityActionNorm);
INFO("Enthalpy-action norm = " << enthalpyActionNorm);
INFO("Displacement-action norm = " << displacementActionNorm);
INFO(
"Complete centered-difference relative error = "
<< completeRelativeError
);
INFO(
"Block-scaled centered-difference error = "
<< blockScaledDifferenceError
);
CHECK(context.GetPreparationCount() == 1);
CHECK(residualEvaluationError < 5.0e-12);
CHECK(blockAssemblyError < 5.0e-12);
CHECK(blockScaledDifferenceError < 2.0e-7);
}
}
}
TEST_CASE(
"Exact Constant Barotropic Closure Remains Zero Under Deformation",
tags::barotrope &tags::closure &tags::hydro &tags::integration
&tags::jacobian &tags::mapping &tags::physics
) {
auto args = test_utils::setup_args();
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.domainMapperStateless != nullptr);
const mean_field::physics::PolytropicBarotrope barotrope(3.0, 1.5);
constexpr double enthalpyValue = 1.20;
const double equilibriumDensityValue =
barotrope.density_from_enthalpy(enthalpyValue);
const mfem::Vector enthalpy =
make_constant_field(*f.enthalpyFes, enthalpyValue);
const mfem::Vector equilibriumDensity =
make_constant_field(*f.densityFes, equilibriumDensityValue);
const mfem::Vector referenceDensity =
make_constant_field(*f.densityFes, equilibriumDensityValue + 1.0);
const mfem::Vector displacementVariation =
gravity_prepared_test_utils::make_displacement(f, 0.67);
const MPI_Comm communicator = f.mesh->GetComm();
for (const double deformationScale : {0.0, 0.5, 1.0}) {
DYNAMIC_SECTION("Deformation scale = " << deformationScale) {
const mfem::Vector displacement =
gravity_prepared_test_utils::make_displacement(
f, deformationScale
);
mfem::Vector exactResidual;
mfem::Vector referenceResidual;
mean_field::operators::kernels::apply_barotropic_closure(
f, *f.domainMapperStateless, barotrope, equilibriumDensity,
enthalpy, displacement, exactResidual
);
mean_field::operators::kernels::apply_barotropic_closure(
f, *f.domainMapperStateless, barotrope, referenceDensity,
enthalpy, displacement, referenceResidual
);
mfem::Vector exactGeometryAction;
mfem::Vector referenceGeometryAction;
mean_field::operators::kernels::
apply_barotropic_closure_displacement_action(
f, *f.domainMapperStateless, barotrope, equilibriumDensity,
enthalpy, displacement, displacementVariation,
exactGeometryAction
);
mean_field::operators::kernels::
apply_barotropic_closure_displacement_action(
f, *f.domainMapperStateless, barotrope, referenceDensity,
enthalpy, displacement, displacementVariation,
referenceGeometryAction
);
const double exactResidualNorm =
gravity_prepared_test_utils::global_norm(
exactResidual, communicator
);
const double referenceResidualNorm =
gravity_prepared_test_utils::global_norm(
referenceResidual, communicator
);
const double exactGeometryNorm =
gravity_prepared_test_utils::global_norm(
exactGeometryAction, communicator
);
const double referenceGeometryNorm =
gravity_prepared_test_utils::global_norm(
referenceGeometryAction, communicator
);
INFO("Deformation scale = " << deformationScale);
INFO("Exact-closure residual norm = " << exactResidualNorm);
INFO("Reference residual norm = " << referenceResidualNorm);
INFO("Exact-closure geometry-action norm = " << exactGeometryNorm);
INFO("Reference geometry-action norm = " << referenceGeometryNorm);
REQUIRE(referenceResidualNorm > 1.0e-12);
REQUIRE(referenceGeometryNorm > 1.0e-14);
CHECK(exactResidualNorm <= 5.0e-12 * referenceResidualNorm);
CHECK(exactGeometryNorm <= 5.0e-12 * referenceGeometryNorm);
}
}
}

View File

@@ -0,0 +1,157 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <mfem.hpp>
import mean_field;
import test_helpers;
using namespace mean_field;
using Catch::Matchers::WithinAbs;
namespace prepared_test = gravity_prepared_test_utils;
TEST_CASE(
"Prepared Mapped Gravity Source Matches Stateless Kernel",
tags::integration &tags::mfem_operators &tags::prepared
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
operators::PreparedMappedGravitySourceOperator prepared_operator(
f, *f.domainMapperStateless
);
REQUIRE(prepared_operator.Width() == f.densityFes->GetTrueVSize());
REQUIRE(
prepared_operator.Height() == f.gravityPotentialFes->GetTrueVSize()
);
const mfem::Vector density = prepared_test::make_deterministic_vector(
f.densityFes->GetTrueVSize(), 0.41
);
const MPI_Comm communicator = f.mesh->GetComm();
mfem::Vector identity_action;
mfem::Vector deformed_action;
for (const double deformation_scale : {0.0, 1.0}) {
const mfem::Vector displacement =
prepared_test::make_displacement(f, deformation_scale);
prepared_operator.Prepare(displacement);
mfem::Vector prepared_action;
mfem::Vector reference_action;
prepared_operator.Mult(density, prepared_action);
operators::kernels::apply_mapped_source(
f, *f.domainMapperStateless, density, displacement, reference_action
);
const double relative_error = prepared_test::relative_error(
prepared_action, reference_action, communicator
);
INFO("Deformation scale = " << deformation_scale);
INFO(
"Prepared source norm = "
<< prepared_test::global_norm(prepared_action, communicator)
);
INFO(
"Reference source norm = "
<< prepared_test::global_norm(reference_action, communicator)
);
INFO("Relative prepared-source error = " << relative_error);
REQUIRE(prepared_operator.IsPrepared());
CHECK_THAT(relative_error, WithinAbs(0.0, 2.0e-11));
if (deformation_scale == 0.0) {
identity_action = prepared_action;
} else {
deformed_action = prepared_action;
}
}
const double geometry_change = prepared_test::relative_error(
deformed_action, identity_action, communicator
);
INFO("Relative source change under deformation = " << geometry_change);
CHECK(prepared_operator.GetPreparationCount() == 2);
CHECK(geometry_change > 1.0e-5);
}
TEST_CASE(
"Prepared Mapped Gravity Source Preserves Linearity And Excludes Vacuum",
tags::integration &tags::gravity &tags::prepared
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
operators::PreparedMappedGravitySourceOperator prepared_operator(
f, *f.domainMapperStateless
);
REQUIRE(prepared_operator.Width() == f.densityFes->GetTrueVSize());
REQUIRE(
prepared_operator.Height() == f.gravityPotentialFes->GetTrueVSize()
);
const mfem::Vector displacement = prepared_test::make_displacement(f, 1.0);
prepared_operator.Prepare(displacement);
const mfem::Vector first = prepared_test::make_deterministic_vector(
f.densityFes->GetTrueVSize(), 0.27
);
const mfem::Vector second = prepared_test::make_deterministic_vector(
f.densityFes->GetTrueVSize(), 0.79
);
const mfem::Vector combination =
prepared_test::linear_combination(first, 1.3, second, -0.6);
const mfem::Vector stellar_density =
prepared_test::make_domain_supported_density(f, true);
const mfem::Vector vacuum_density =
prepared_test::make_domain_supported_density(f, false);
mfem::Vector first_action;
mfem::Vector second_action;
mfem::Vector combination_action;
mfem::Vector stellar_action;
mfem::Vector vacuum_action;
prepared_operator.Mult(first, first_action);
prepared_operator.Mult(second, second_action);
prepared_operator.Mult(combination, combination_action);
prepared_operator.Mult(stellar_density, stellar_action);
prepared_operator.Mult(vacuum_density, vacuum_action);
const mfem::Vector expected_combination = prepared_test::linear_combination(
first_action, 1.3, second_action, -0.6
);
const MPI_Comm communicator = f.mesh->GetComm();
const double linearity_error = prepared_test::relative_error(
combination_action, expected_combination, communicator
);
const double stellar_norm =
prepared_test::global_norm(stellar_action, communicator);
const double vacuum_norm =
prepared_test::global_norm(vacuum_action, communicator);
const std::uint64_t preparation_count =
prepared_operator.GetPreparationCount();
mfem::Vector repeated_action;
prepared_operator.Mult(first, repeated_action);
INFO("Relative source linearity error = " << linearity_error);
INFO("Stellar source norm = " << stellar_norm);
INFO("Vacuum-only source norm = " << vacuum_norm);
CHECK_THAT(linearity_error, WithinAbs(0.0, 2.0e-12));
CHECK(stellar_norm > 0.0);
CHECK(vacuum_norm <= 1.0e-13 * stellar_norm);
CHECK(
prepared_test::relative_error(
repeated_action, first_action, communicator
) < 2.0e-14
);
CHECK(prepared_operator.GetPreparationCount() == preparation_count);
}

View File

@@ -0,0 +1,164 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <mfem.hpp>
import mean_field;
import test_helpers;
using namespace mean_field;
using Catch::Matchers::WithinAbs;
namespace prepared_test = gravity_prepared_test_utils;
TEST_CASE(
"Prepared Mapped Hdiv Mass Matches Stateless Kernel",
tags::integration &tags::mfem_operators &tags::prepared
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
operators::PreparedMappedHDivMassOperator prepared_operator(
f, *f.domainMapperStateless
);
const mfem::Vector gravity_gradient =
prepared_test::make_deterministic_vector(
f.gravityFluxFes->GetTrueVSize(), 0.21
);
const MPI_Comm communicator = f.gravityFluxFes->GetComm();
mfem::Vector identity_action;
mfem::Vector deformed_action;
for (const double deformation_scale : {0.0, 1.0}) {
const mfem::Vector displacement =
prepared_test::make_displacement(f, deformation_scale);
prepared_operator.Prepare(displacement);
mfem::Vector prepared_action;
mfem::Vector reference_action;
prepared_operator.Mult(gravity_gradient, prepared_action);
operators::kernels::apply_mapped_hdiv_mass(
f, *f.domainMapperStateless, gravity_gradient, displacement,
reference_action
);
const double relative_error = prepared_test::relative_error(
prepared_action, reference_action, communicator
);
INFO("Deformation scale = " << deformation_scale);
INFO(
"Prepared action norm = "
<< prepared_test::global_norm(prepared_action, communicator)
);
INFO(
"Reference action norm = "
<< prepared_test::global_norm(reference_action, communicator)
);
INFO("Relative prepared-operator error = " << relative_error);
REQUIRE(prepared_operator.IsPrepared());
CHECK_THAT(relative_error, WithinAbs(0.0, 2.0e-11));
if (deformation_scale == 0.0) {
identity_action = prepared_action;
} else {
deformed_action = prepared_action;
}
}
const double geometry_change = prepared_test::relative_error(
deformed_action, identity_action, communicator
);
INFO("Relative action change under deformation = " << geometry_change);
CHECK(prepared_operator.GetPreparationCount() == 2);
CHECK(geometry_change > 1.0e-5);
}
TEST_CASE(
"Prepared Mapped Hdiv Mass Preserves Operator Identities",
tags::integration &tags::gravity &tags::prepared
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
operators::PreparedMappedHDivMassOperator prepared_operator(
f, *f.domainMapperStateless
);
const mfem::Vector displacement = prepared_test::make_displacement(f, 1.0);
prepared_operator.Prepare(displacement);
const mfem::Vector first = prepared_test::make_deterministic_vector(
f.gravityFluxFes->GetTrueVSize(), 0.17
);
const mfem::Vector second = prepared_test::make_deterministic_vector(
f.gravityFluxFes->GetTrueVSize(), 0.83
);
const mfem::Vector combination =
prepared_test::linear_combination(first, 1.7, second, -0.4);
mfem::Vector first_action;
mfem::Vector second_action;
mfem::Vector combination_action;
mfem::Vector zero_action;
prepared_operator.Mult(first, first_action);
prepared_operator.Mult(second, second_action);
prepared_operator.Mult(combination, combination_action);
mfem::Vector expected_combination = prepared_test::linear_combination(
first_action, 1.7, second_action, -0.4
);
mfem::Vector zero(first.Size());
zero = 0.0;
prepared_operator.Mult(zero, zero_action);
const MPI_Comm communicator = f.gravityFluxFes->GetComm();
const double first_second_product =
prepared_test::global_dot(first, second_action, communicator);
const double second_first_product =
prepared_test::global_dot(second, first_action, communicator);
const double symmetry_error = prepared_test::relative_scalar_error(
first_second_product, second_first_product
);
const double linearity_error = prepared_test::relative_error(
combination_action, expected_combination, communicator
);
const double first_energy =
prepared_test::global_dot(first, first_action, communicator);
const double second_energy =
prepared_test::global_dot(second, second_action, communicator);
const std::uint64_t preparation_count =
prepared_operator.GetPreparationCount();
mfem::Vector repeated_action;
prepared_operator.Mult(first, repeated_action);
INFO("u^T M v = " << first_second_product);
INFO("v^T M u = " << second_first_product);
INFO("Relative symmetry error = " << symmetry_error);
INFO("Relative linearity error = " << linearity_error);
INFO("u^T M u = " << first_energy);
INFO("v^T M v = " << second_energy);
CHECK_THAT(symmetry_error, WithinAbs(0.0, 2.0e-12));
CHECK_THAT(linearity_error, WithinAbs(0.0, 2.0e-12));
CHECK_THAT(
prepared_test::global_norm(zero_action, communicator),
WithinAbs(0.0, 1.0e-14)
);
CHECK(first_energy > 0.0);
CHECK(second_energy > 0.0);
CHECK(
prepared_test::relative_error(
repeated_action, first_action, communicator
) < 2.0e-14
);
CHECK(prepared_operator.GetPreparationCount() == preparation_count);
}

View File

@@ -0,0 +1,334 @@
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
import mean_field;
import test_helpers;
namespace prepared_hydrostatic_test_utils {
static mean_field::operators::context::hydrostatic::
HydrostaticEquilibriumDependencies
make_dependencies() {
return {
.discretization = {.identity = 211, .revision = 2},
.enthalpy = {.identity = 223, .revision = 3},
.gravityPotential = {.identity = 227, .revision = 5},
.displacement = {.identity = 229, .revision = 7},
.rotation = {.identity = 233, .revision = 11},
.bernoulliConstant = {.identity = 239, .revision = 13}
};
}
mean_field::operators::context::hydrostatic::HydrostaticEquilibriumStateView
make_state(
const mfem::Vector &enthalpy,
const mfem::Vector &gravityPotential,
const mfem::Vector &displacement,
const double bernoulliConstant
) {
return {
.enthalpy = enthalpy,
.gravityPotential = gravityPotential,
.displacement = displacement,
.bernoulliConstant = bernoulliConstant
};
}
mfem::Vector make_enthalpy(
const mean_field::fem::FEM &f,
const double phase = 0.19
) {
return gravity_prepared_test_utils::make_deterministic_vector(
f.enthalpyFes->GetTrueVSize(), phase
);
}
mfem::Vector make_gravity_potential(
const mean_field::fem::FEM &f,
const double phase = 0.37
) {
return gravity_prepared_test_utils::make_deterministic_vector(
f.gravityPotentialFes->GetTrueVSize(), phase
);
}
mean_field::physics::RigidRotation make_rotation(const double scale = 1.0) {
mfem::Vector angularVelocity(3);
angularVelocity(0) = 0.17 * scale;
angularVelocity(1) = -0.11 * scale;
angularVelocity(2) = 0.43 * scale;
mfem::Vector center(3);
center(0) = 0.037;
center(1) = -0.029;
center(2) = 0.021;
return mean_field::physics::RigidRotation(angularVelocity, center);
}
} // namespace prepared_hydrostatic_test_utils
TEST_CASE(
"Prepared Hydrostatic Residual Matches Stateless Kernel",
tags::barotrope &tags::hydro &tags::prepared &tags::residuals &tags::unit
) {
auto args = test_utils::setup_args();
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
mean_field::operators::PreparedHydrostaticEquilibriumOperator
preparedOperator(f, *f.domainMapperStateless);
const mfem::Vector enthalpy =
prepared_hydrostatic_test_utils::make_enthalpy(f);
const mfem::Vector gravityPotential =
prepared_hydrostatic_test_utils::make_gravity_potential(f);
const mfem::Vector displacement =
gravity_prepared_test_utils::make_displacement(f, 0.73);
constexpr double bernoulliConstant = 0.41;
const mean_field::physics::RigidRotation rotation =
prepared_hydrostatic_test_utils::make_rotation();
const auto dependencies =
prepared_hydrostatic_test_utils::make_dependencies();
CHECK_FALSE(preparedOperator.IsPrepared());
CHECK(preparedOperator.GetResidualPreparationCount() == 0);
CHECK(preparedOperator.GetResidualApplicationCount() == 0);
const auto report = preparedOperator.Prepare(
prepared_hydrostatic_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
dependencies, rotation
);
mfem::Vector preparedResidual;
mfem::Vector referenceResidual;
preparedOperator.BuildResidual(preparedResidual);
mean_field::operators::kernels::apply_hydrostatic_equilibrium(
f, *f.domainMapperStateless, rotation, enthalpy, gravityPotential,
displacement, bernoulliConstant, referenceResidual
);
const double relativeError = gravity_prepared_test_utils::relative_error(
preparedResidual, referenceResidual, f.mesh->GetComm()
);
INFO("Prepared hydrostatic residual relative error = " << relativeError);
const auto &statistics = preparedOperator.GetContextPreparationStatistics();
CHECK(preparedOperator.IsPrepared());
CHECK(report.contextReport.preparedStaticDependencies);
CHECK(report.contextReport.preparedGeometryState);
CHECK(report.contextReport.preparedRotationDependencies);
CHECK(report.contextReport.preparedBaseState);
CHECK(report.updatedRotation);
CHECK(report.preparedResidual);
CHECK(report.DidAnyWork());
CHECK(preparedOperator.GetStellarElementCount() > 0);
CHECK(statistics.staticPreparations == 1);
CHECK(statistics.geometryPreparations == 1);
CHECK(statistics.rotationPreparations == 1);
CHECK(statistics.baseStatePreparations == 1);
CHECK(preparedOperator.GetResidualPreparationCount() == 1);
CHECK(preparedOperator.GetResidualApplicationCount() == 1);
CHECK(relativeError < 2.0e-12);
}
TEST_CASE(
"Prepared Hydrostatic Residual Reuses And Selectively Rebuilds Data",
tags::barotrope &tags::hydro &tags::prepared &tags::residuals &tags::unit
) {
auto args = test_utils::setup_args();
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
mean_field::operators::PreparedHydrostaticEquilibriumOperator
preparedOperator(f, *f.domainMapperStateless);
mfem::Vector enthalpy = prepared_hydrostatic_test_utils::make_enthalpy(f);
mfem::Vector gravityPotential =
prepared_hydrostatic_test_utils::make_gravity_potential(f);
mfem::Vector displacement =
gravity_prepared_test_utils::make_displacement(f, 0.42);
double bernoulliConstant = 0.36;
const mfem::Vector initialEnthalpy(enthalpy);
const mfem::Vector initialGravityPotential(gravityPotential);
const mfem::Vector initialDisplacement(displacement);
const double initialBernoulliConstant = bernoulliConstant;
const mean_field::physics::RigidRotation initialRotation =
prepared_hydrostatic_test_utils::make_rotation(0.80);
const mean_field::physics::RigidRotation changedRotation =
prepared_hydrostatic_test_utils::make_rotation(1.25);
auto dependencies = prepared_hydrostatic_test_utils::make_dependencies();
preparedOperator.Prepare(
prepared_hydrostatic_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
dependencies, initialRotation
);
mfem::Vector initialResidual;
preparedOperator.BuildResidual(initialResidual);
enthalpy(0) += 0.29;
gravityPotential(0) -= 0.17;
displacement = gravity_prepared_test_utils::make_displacement(f, 0.73);
bernoulliConstant += 0.23;
const auto unchangedReport = preparedOperator.Prepare(
prepared_hydrostatic_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
dependencies, changedRotation
);
mfem::Vector unchangedResidual;
preparedOperator.BuildResidual(unchangedResidual);
CHECK_FALSE(unchangedReport.DidAnyWork());
CHECK_FALSE(unchangedReport.updatedRotation);
CHECK_FALSE(unchangedReport.preparedResidual);
CHECK(
gravity_prepared_test_utils::relative_error(
unchangedResidual, initialResidual, f.mesh->GetComm()
) == 0.0
);
CHECK(preparedOperator.GetResidualPreparationCount() == 1);
// Only the enthalpy stamp changes. The altered potential,
// displacement, constant, and rotation remain intentionally frozen.
++dependencies.enthalpy.revision;
const auto enthalpyReport = preparedOperator.Prepare(
prepared_hydrostatic_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
dependencies, changedRotation
);
mfem::Vector enthalpyResidual;
mfem::Vector enthalpyReference;
preparedOperator.BuildResidual(enthalpyResidual);
mean_field::operators::kernels::apply_hydrostatic_equilibrium(
f, *f.domainMapperStateless, initialRotation, enthalpy,
initialGravityPotential, initialDisplacement, initialBernoulliConstant,
enthalpyReference
);
CHECK_FALSE(enthalpyReport.contextReport.preparedStaticDependencies);
CHECK_FALSE(enthalpyReport.contextReport.preparedGeometryState);
CHECK_FALSE(enthalpyReport.contextReport.preparedRotationDependencies);
CHECK(enthalpyReport.contextReport.preparedBaseState);
CHECK_FALSE(enthalpyReport.updatedRotation);
CHECK(enthalpyReport.preparedResidual);
CHECK(
gravity_prepared_test_utils::relative_error(
enthalpyResidual, enthalpyReference, f.mesh->GetComm()
) < 2.0e-12
);
++dependencies.rotation.revision;
const auto rotationReport = preparedOperator.Prepare(
prepared_hydrostatic_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
dependencies, changedRotation
);
mfem::Vector rotationResidual;
mfem::Vector rotationReference;
preparedOperator.BuildResidual(rotationResidual);
mean_field::operators::kernels::apply_hydrostatic_equilibrium(
f, *f.domainMapperStateless, changedRotation, enthalpy,
initialGravityPotential, initialDisplacement, initialBernoulliConstant,
rotationReference
);
CHECK_FALSE(rotationReport.contextReport.preparedStaticDependencies);
CHECK_FALSE(rotationReport.contextReport.preparedGeometryState);
CHECK(rotationReport.contextReport.preparedRotationDependencies);
CHECK(rotationReport.contextReport.preparedBaseState);
CHECK(rotationReport.updatedRotation);
CHECK(rotationReport.preparedResidual);
CHECK(
gravity_prepared_test_utils::relative_error(
rotationResidual, rotationReference, f.mesh->GetComm()
) < 2.0e-12
);
++dependencies.displacement.revision;
const auto displacementReport = preparedOperator.Prepare(
prepared_hydrostatic_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
dependencies, changedRotation
);
mfem::Vector displacementResidual;
mfem::Vector displacementReference;
preparedOperator.BuildResidual(displacementResidual);
mean_field::operators::kernels::apply_hydrostatic_equilibrium(
f, *f.domainMapperStateless, changedRotation, enthalpy,
initialGravityPotential, displacement, initialBernoulliConstant,
displacementReference
);
CHECK_FALSE(displacementReport.contextReport.preparedStaticDependencies);
CHECK(displacementReport.contextReport.preparedGeometryState);
CHECK(displacementReport.contextReport.preparedRotationDependencies);
CHECK(displacementReport.contextReport.preparedBaseState);
CHECK_FALSE(displacementReport.updatedRotation);
CHECK(displacementReport.preparedResidual);
CHECK(
gravity_prepared_test_utils::relative_error(
displacementResidual, displacementReference, f.mesh->GetComm()
) < 2.0e-12
);
const auto &statistics = preparedOperator.GetContextPreparationStatistics();
CHECK(statistics.staticPreparations == 1);
CHECK(statistics.geometryPreparations == 2);
CHECK(statistics.rotationPreparations == 3);
CHECK(statistics.baseStatePreparations == 4);
CHECK(preparedOperator.GetResidualPreparationCount() == 4);
CHECK(preparedOperator.GetResidualApplicationCount() == 5);
const double displacementEffect =
gravity_prepared_test_utils::relative_error(
displacementResidual, rotationResidual, f.mesh->GetComm()
);
INFO("Residual change after displacement update = " << displacementEffect);
CHECK(displacementEffect > 1.0e-8);
}

View File

@@ -0,0 +1,484 @@
#include <algorithm>
#include <array>
#include <cmath>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
import mean_field;
import test_helpers;
namespace prepared_hydrostatic_analytic_solve_test_utils {
constexpr double bernoulliConstant = 0.83;
constexpr double enthalpyAmplitude = 0.61;
struct AnalyticCase {
const char *name;
std::array<double, 3> deformationScale;
std::array<double, 3> angularVelocity;
std::array<double, 3> rotationCenter;
};
class EnthalpyJacobianOperator final : public mfem::Operator {
public:
EnthalpyJacobianOperator(
const int enthalpySize,
const mean_field::operators::PreparedHydrostaticEquilibriumOperator
&preparedOperator
)
: mfem::Operator(enthalpySize),
m_preparedOperator(preparedOperator) {
}
void Mult(
const mfem::Vector &direction,
mfem::Vector &action
) const override {
m_preparedOperator.ApplyEnthalpyJacobianAction(direction, action);
}
private:
const mean_field::operators::PreparedHydrostaticEquilibriumOperator
&m_preparedOperator;
};
mean_field::operators::context::hydrostatic::
HydrostaticEquilibriumDependencies
make_dependencies() {
return {
.discretization = {.identity = 701, .revision = 2},
.enthalpy = {.identity = 709, .revision = 3},
.gravityPotential = {.identity = 719, .revision = 5},
.displacement = {.identity = 727, .revision = 7},
.rotation = {.identity = 733, .revision = 11},
.bernoulliConstant = {.identity = 739, .revision = 13}
};
}
mean_field::operators::context::hydrostatic::HydrostaticEquilibriumStateView
make_state(
const mfem::Vector &enthalpy,
const mfem::Vector &gravityPotential,
const mfem::Vector &displacement
) {
return {
.enthalpy = enthalpy,
.gravityPotential = gravityPotential,
.displacement = displacement,
.bernoulliConstant = bernoulliConstant
};
}
mfem::Vector make_vector(
const std::array<
double,
3> &values
) {
mfem::Vector vector(3);
for (int component = 0; component < 3; ++component) {
vector(component) = values[static_cast<std::size_t>(component)];
}
return vector;
}
mean_field::physics::RigidRotation
make_rotation(const AnalyticCase &analyticCase) {
return mean_field::physics::RigidRotation(
make_vector(analyticCase.angularVelocity),
make_vector(analyticCase.rotationCenter)
);
}
void map_to_physical(
const mfem::Vector &referencePosition,
const AnalyticCase &analyticCase,
mfem::Vector &physicalPosition
) {
physicalPosition.SetSize(3);
for (int component = 0; component < 3; ++component) {
physicalPosition(component) =
analyticCase
.deformationScale[static_cast<std::size_t>(component)] *
referencePosition(component);
}
}
double exact_enthalpy_value(const mfem::Vector &referencePosition) {
double normalizedRadiusSquared = 0.0;
for (int component = 0; component < 3; ++component) {
const double normalizedCoordinate =
referencePosition(component) / mean_field::utils::RADIUS;
normalizedRadiusSquared +=
normalizedCoordinate * normalizedCoordinate;
}
return enthalpyAmplitude * std::max(0.0, 1.0 - normalizedRadiusSquared);
}
double exact_potential_value(
const mfem::Vector &referencePosition,
const AnalyticCase &analyticCase,
const mean_field::physics::RigidRotation &rotation
) {
mfem::Vector physicalPosition;
map_to_physical(referencePosition, analyticCase, physicalPosition);
/*
* Construct Phi so that
*
* h + Phi - Psi_rotation - C = 0
*
* analytically.
*/
return bernoulliConstant + rotation.potential(physicalPosition) -
exact_enthalpy_value(referencePosition);
}
mfem::Array<int>
make_stellar_element_marker(const mean_field::fem::FEM &f) {
mfem::Array<int> stellarElementMarker(f.mesh->GetNE());
const int vacuumAttribute =
f.domainMapperStateless->GetVacuumElementAttribute();
for (int elementId = 0; elementId < f.mesh->GetNE(); ++elementId) {
stellarElementMarker[elementId] =
f.mesh->GetAttribute(elementId) != vacuumAttribute;
}
return stellarElementMarker;
}
} // namespace prepared_hydrostatic_analytic_solve_test_utils
TEST_CASE(
"Prepared Hydrostatic Operator Solves Analytic Bernoulli Equilibria",
tags::barotrope &tags::hydro &tags::prepared &tags::integration
&tags::solver &tags::convergence &tags::accuracy
&tags::analytic_comparison
) {
using prepared_hydrostatic_analytic_solve_test_utils::AnalyticCase;
constexpr double deformationX = 1.08;
constexpr double deformationY = 0.96;
/*
* The third scale makes the affine deformation
* volume-preserving:
*
* det(F) = sx * sy * sz = 1.
*/
constexpr double deformationZ = 1.0 / (deformationX * deformationY);
const std::array<AnalyticCase, 3> analyticCases{
{{.name = "spherical nonrotating equilibrium",
.deformationScale = {1.0, 1.0, 1.0},
.angularVelocity = {0.0, 0.0, 0.0},
.rotationCenter = {0.0, 0.0, 0.0}},
{.name = "spherical rotating equilibrium",
.deformationScale = {1.0, 1.0, 1.0},
.angularVelocity = {0.13, -0.09, 0.31},
.rotationCenter = {0.04, -0.03, 0.02}},
{.name = "volume-preserving deformed rotating equilibrium",
.deformationScale = {deformationX, deformationY, deformationZ},
.angularVelocity = {0.17, -0.12, 0.43},
.rotationCenter = {0.031, -0.024, 0.018}}}
};
auto args = test_utils::setup_args();
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
const MPI_Comm communicator = f.mesh->GetComm();
const mfem::Array<int> stellarElementMarker =
prepared_hydrostatic_analytic_solve_test_utils::
make_stellar_element_marker(f);
for (const AnalyticCase &analyticCase : analyticCases) {
DYNAMIC_SECTION(analyticCase.name) {
const double deformationDeterminant =
analyticCase.deformationScale[0] *
analyticCase.deformationScale[1] *
analyticCase.deformationScale[2];
REQUIRE(std::abs(deformationDeterminant - 1.0) < 2.0e-14);
const mean_field::physics::RigidRotation rotation =
prepared_hydrostatic_analytic_solve_test_utils::make_rotation(
analyticCase
);
auto displacementFunction = [&analyticCase](
const mfem::Vector
&referencePosition,
mfem::Vector &displacementValue
) {
mfem::Vector physicalPosition;
prepared_hydrostatic_analytic_solve_test_utils::map_to_physical(
referencePosition, analyticCase, physicalPosition
);
displacementValue.SetSize(3);
displacementValue = physicalPosition;
displacementValue -= referencePosition;
};
auto potentialFunction = [&analyticCase, &rotation](
const mfem::Vector &referencePosition
) {
return prepared_hydrostatic_analytic_solve_test_utils::
exact_potential_value(
referencePosition, analyticCase, rotation
);
};
auto enthalpyFunction = [](const mfem::Vector &referencePosition) {
return prepared_hydrostatic_analytic_solve_test_utils::
exact_enthalpy_value(referencePosition);
};
mfem::VectorFunctionCoefficient displacementCoefficient(
f.mesh->Dimension(), displacementFunction
);
mfem::FunctionCoefficient potentialCoefficient(potentialFunction);
mfem::FunctionCoefficient exactEnthalpyCoefficient(
enthalpyFunction
);
/*
* Project the prescribed geometry and potential.
*/
mfem::ParGridFunction displacementField(f.displacementFes.get());
mfem::ParGridFunction potentialField(f.gravityPotentialFes.get());
displacementField.ProjectCoefficient(displacementCoefficient);
potentialField.ProjectCoefficient(potentialCoefficient);
mfem::Vector displacement;
mfem::Vector gravityPotential;
displacementField.GetTrueDofs(displacement);
potentialField.GetTrueDofs(gravityPotential);
/*
* This projection is not used as the solution. It gives
* the best directly available representation baseline
* against which the solved field can be compared.
*/
mfem::ParGridFunction projectedEnthalpyField(f.enthalpyFes.get());
projectedEnthalpyField.ProjectCoefficient(exactEnthalpyCoefficient);
mfem::ParGridFunction zeroEnthalpyField(f.enthalpyFes.get());
zeroEnthalpyField = 0.0;
const double exactEnthalpyNorm = zeroEnthalpyField.ComputeL2Error(
exactEnthalpyCoefficient, nullptr, &stellarElementMarker
);
const double projectionError =
projectedEnthalpyField.ComputeL2Error(
exactEnthalpyCoefficient, nullptr, &stellarElementMarker
);
REQUIRE(exactEnthalpyNorm > 0.0);
const double relativeProjectionError =
projectionError / exactEnthalpyNorm;
/*
* Begin deliberately far from equilibrium.
*/
mfem::Vector enthalpy(f.enthalpyFes->GetTrueVSize());
enthalpy = 0.0;
auto dependencies = prepared_hydrostatic_analytic_solve_test_utils::
make_dependencies();
mean_field::operators::PreparedHydrostaticEquilibriumOperator
preparedOperator(f, *f.domainMapperStateless);
const auto initialReport = preparedOperator.Prepare(
prepared_hydrostatic_analytic_solve_test_utils::make_state(
enthalpy, gravityPotential, displacement
),
dependencies, rotation
);
REQUIRE(initialReport.preparedResidual);
REQUIRE(initialReport.preparedAlgebraicJacobianBlocks);
mfem::Vector initialResidual;
preparedOperator.BuildResidual(initialResidual);
const double initialResidualNorm =
gravity_prepared_test_utils::global_norm(
initialResidual, communicator
);
REQUIRE(initialResidualNorm > 1.0e-12);
/*
* One discrete Newton step:
*
* M_h delta_h = -R_h.
*
* The full four-block Bernoulli Jacobian is rectangular
* and underdetermined in isolation. Freezing Phi, C,
* rotation, and displacement makes this a well-defined
* enthalpy solve.
*/
prepared_hydrostatic_analytic_solve_test_utils::
EnthalpyJacobianOperator enthalpyJacobian(
f.enthalpyFes->GetTrueVSize(), preparedOperator
);
mfem::Vector rightHandSide(initialResidual);
rightHandSide *= -1.0;
mfem::Vector enthalpyCorrection(f.enthalpyFes->GetTrueVSize());
enthalpyCorrection = 0.0;
/*
* The operator is positive definite on stellar-supported
* enthalpy DOFs and semidefinite on exterior-only DOFs.
* The RHS is in its range, so MINRES is appropriate for
* the compatible system.
*/
mfem::MINRESSolver linearSolver(communicator);
linearSolver.SetOperator(enthalpyJacobian);
linearSolver.SetRelTol(1.0e-13);
linearSolver.SetAbsTol(1.0e-14);
linearSolver.SetMaxIter(2000);
linearSolver.SetPrintLevel(0);
linearSolver.Mult(rightHandSide, enthalpyCorrection);
INFO("Linear solver converged = " << linearSolver.GetConverged());
INFO(
"Linear solver iterations = " << linearSolver.GetNumIterations()
);
INFO("Linear solver final norm = " << linearSolver.GetFinalNorm());
REQUIRE(linearSolver.GetConverged());
enthalpy += enthalpyCorrection;
/*
* Only the enthalpy state changed. Geometry, rotation,
* and algebraic Jacobian data must remain reusable.
*/
++dependencies.enthalpy.revision;
const auto solvedReport = preparedOperator.Prepare(
prepared_hydrostatic_analytic_solve_test_utils::make_state(
enthalpy, gravityPotential, displacement
),
dependencies, rotation
);
CHECK(solvedReport.contextReport.updatedEnthalpy);
CHECK(solvedReport.contextReport.preparedBaseState);
CHECK_FALSE(solvedReport.contextReport.preparedGeometryState);
CHECK_FALSE(solvedReport.preparedAlgebraicJacobianBlocks);
mfem::Vector solvedResidual;
preparedOperator.BuildResidual(solvedResidual);
const double solvedResidualNorm =
gravity_prepared_test_utils::global_norm(
solvedResidual, communicator
);
const double residualReduction =
solvedResidualNorm / initialResidualNorm;
/*
* Compare the solved field with the continuum analytic
* enthalpy over stellar elements only.
*
* All three mappings have determinant one, so this
* normalized L2 error is also unchanged by the physical
* volume transformation.
*/
mfem::ParGridFunction solvedEnthalpyField(f.enthalpyFes.get());
solvedEnthalpyField.SetFromTrueDofs(enthalpy);
const double solvedAnalyticError =
solvedEnthalpyField.ComputeL2Error(
exactEnthalpyCoefficient, nullptr, &stellarElementMarker
);
const double relativeSolvedAnalyticError =
solvedAnalyticError / exactEnthalpyNorm;
INFO("Deformation determinant = " << deformationDeterminant);
INFO("Initial weak residual norm = " << initialResidualNorm);
INFO("Solved weak residual norm = " << solvedResidualNorm);
INFO("Weak residual reduction = " << residualReduction);
INFO(
"Relative analytic projection floor = "
<< relativeProjectionError
);
INFO(
"Relative solved analytic L2 error = "
<< relativeSolvedAnalyticError
);
/*
* The discrete Bernoulli equation must be solved essentially
* to the linear-solver floor.
*/
CHECK(residualReduction < 1.0e-10);
/*
* The directly projected analytic enthalpy provides a lower
* representation bound, but it is not the expected solution
* of the cross-space discrete Bernoulli equation. The latter
* also contains potential-projection and mapped-space
* compatibility errors.
*/
CHECK(
relativeSolvedAnalyticError <
std::max(5.0 * relativeProjectionError, 1.25e-4)
);
/*
* Record that the analytic error remains within one order of
* magnitude of the direct enthalpy projection floor.
*/
CHECK(relativeSolvedAnalyticError / relativeProjectionError < 5.0);
}
}
}

View File

@@ -0,0 +1,436 @@
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
import mean_field;
import test_helpers;
namespace prepared_hydrostatic_complete_test_utils {
mean_field::operators::context::hydrostatic::
HydrostaticEquilibriumDependencies
make_dependencies() {
return {
.discretization = {.identity = 503, .revision = 2},
.enthalpy = {.identity = 509, .revision = 3},
.gravityPotential = {.identity = 521, .revision = 5},
.displacement = {.identity = 523, .revision = 7},
.rotation = {.identity = 541, .revision = 11},
.bernoulliConstant = {.identity = 547, .revision = 13}
};
}
mean_field::operators::context::hydrostatic::HydrostaticEquilibriumStateView
make_state(
const mfem::Vector &enthalpy,
const mfem::Vector &gravityPotential,
const mfem::Vector &displacement,
const double bernoulliConstant
) {
return {
.enthalpy = enthalpy,
.gravityPotential = gravityPotential,
.displacement = displacement,
.bernoulliConstant = bernoulliConstant
};
}
mean_field::physics::RigidRotation make_rotation() {
mfem::Vector angularVelocity(3);
angularVelocity(0) = 0.18;
angularVelocity(1) = -0.13;
angularVelocity(2) = 0.49;
mfem::Vector center(3);
center(0) = 0.031;
center(1) = -0.024;
center(2) = 0.017;
return mean_field::physics::RigidRotation(angularVelocity, center);
}
mfem::Vector make_displacement_direction(
const mean_field::fem::FEM &f,
const double firstPhase,
const double secondPhase
) {
mfem::Vector direction =
gravity_prepared_test_utils::make_displacement(f, firstPhase);
const mfem::Vector secondField =
gravity_prepared_test_utils::make_displacement(f, secondPhase);
direction -= secondField;
return direction;
}
void centered_complete_difference(
const mean_field::fem::FEM &f,
const mean_field::physics::RigidRotation &rotation,
const mfem::Vector &baseEnthalpy,
const mfem::Vector &baseGravityPotential,
const mfem::Vector &baseDisplacement,
const double baseBernoulliConstant,
const mfem::Vector &enthalpyVariation,
const mfem::Vector &gravityPotentialVariation,
const mfem::Vector &displacementVariation,
const double bernoulliConstantVariation,
const double step,
mfem::Vector &difference
) {
mfem::Vector enthalpyPlus(baseEnthalpy);
mfem::Vector enthalpyMinus(baseEnthalpy);
mfem::Vector gravityPotentialPlus(baseGravityPotential);
mfem::Vector gravityPotentialMinus(baseGravityPotential);
mfem::Vector displacementPlus(baseDisplacement);
mfem::Vector displacementMinus(baseDisplacement);
enthalpyPlus.Add(step, enthalpyVariation);
enthalpyMinus.Add(-step, enthalpyVariation);
gravityPotentialPlus.Add(step, gravityPotentialVariation);
gravityPotentialMinus.Add(-step, gravityPotentialVariation);
displacementPlus.Add(step, displacementVariation);
displacementMinus.Add(-step, displacementVariation);
const double bernoulliConstantPlus =
baseBernoulliConstant + step * bernoulliConstantVariation;
const double bernoulliConstantMinus =
baseBernoulliConstant - step * bernoulliConstantVariation;
mfem::Vector residualPlus;
mfem::Vector residualMinus;
mean_field::operators::kernels::apply_hydrostatic_equilibrium(
f, *f.domainMapperStateless, rotation, enthalpyPlus,
gravityPotentialPlus, displacementPlus, bernoulliConstantPlus,
residualPlus
);
mean_field::operators::kernels::apply_hydrostatic_equilibrium(
f, *f.domainMapperStateless, rotation, enthalpyMinus,
gravityPotentialMinus, displacementMinus, bernoulliConstantMinus,
residualMinus
);
difference = residualPlus;
difference -= residualMinus;
difference /= 2.0 * step;
}
void set_block(
mfem::Vector &packedDirection,
const mean_field::operators::HydrostaticJacobianBlockLayout &layout,
const mean_field::operators::HydrostaticJacobianInputBlock block,
const mfem::Vector &values
) {
REQUIRE(layout.Size(block) == values.Size());
const int offset = layout.Offset(block);
for (int entry = 0; entry < values.Size(); ++entry) {
packedDirection(offset + entry) = values(entry);
}
}
} // namespace prepared_hydrostatic_complete_test_utils
TEST_CASE(
"Prepared Hydrostatic Complete Jacobian Matches Sum And Centered "
"Differences",
tags::barotrope &tags::hydro &tags::integration &tags::jacobian
&tags::prepared &tags::self_consistency
) {
auto args = test_utils::setup_args();
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
mean_field::operators::PreparedHydrostaticEquilibriumOperator
preparedOperator(f, *f.domainMapperStateless);
const mfem::Vector enthalpy =
gravity_prepared_test_utils::make_deterministic_vector(
f.enthalpyFes->GetTrueVSize(), 0.34
);
const mfem::Vector gravityPotential =
gravity_prepared_test_utils::make_deterministic_vector(
f.gravityPotentialFes->GetTrueVSize(), 0.57
);
const mfem::Vector displacement =
gravity_prepared_test_utils::make_displacement(f, 0.68);
constexpr double bernoulliConstant = 0.43;
const mean_field::physics::RigidRotation rotation =
prepared_hydrostatic_complete_test_utils::make_rotation();
preparedOperator.Prepare(
prepared_hydrostatic_complete_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
prepared_hydrostatic_complete_test_utils::make_dependencies(), rotation
);
const mfem::Vector enthalpyVariation =
gravity_prepared_test_utils::make_deterministic_vector(
f.enthalpyFes->GetTrueVSize(), 1.07
);
const mfem::Vector gravityPotentialVariation =
gravity_prepared_test_utils::make_deterministic_vector(
f.gravityPotentialFes->GetTrueVSize(), 1.31
);
const mfem::Vector displacementVariation =
prepared_hydrostatic_complete_test_utils::make_displacement_direction(
f, 1.19, 0.38
);
constexpr double bernoulliConstantVariation = -0.37;
mfem::Vector enthalpyAction;
mfem::Vector gravityPotentialAction;
mfem::Vector bernoulliConstantAction;
mfem::Vector displacementAction;
mfem::Vector completeAction;
preparedOperator.ApplyEnthalpyJacobianAction(
enthalpyVariation, enthalpyAction
);
preparedOperator.ApplyGravityPotentialJacobianAction(
gravityPotentialVariation, gravityPotentialAction
);
preparedOperator.ApplyBernoulliConstantJacobianAction(
bernoulliConstantVariation, bernoulliConstantAction
);
preparedOperator.ApplyDisplacementJacobianAction(
displacementVariation, displacementAction
);
preparedOperator.ApplyCompleteJacobianAction(
enthalpyVariation, gravityPotentialVariation,
bernoulliConstantVariation, displacementVariation, completeAction
);
mfem::Vector summedAction(enthalpyAction);
summedAction += gravityPotentialAction;
summedAction += bernoulliConstantAction;
summedAction += displacementAction;
constexpr double finiteDifferenceStep = 1.0e-5;
mfem::Vector centeredDifference;
prepared_hydrostatic_complete_test_utils::centered_complete_difference(
f, rotation, enthalpy, gravityPotential, displacement,
bernoulliConstant, enthalpyVariation, gravityPotentialVariation,
displacementVariation, bernoulliConstantVariation, finiteDifferenceStep,
centeredDifference
);
const double summationError = gravity_prepared_test_utils::relative_error(
completeAction, summedAction, f.mesh->GetComm()
);
const double centeredDifferenceError =
gravity_prepared_test_utils::relative_error(
completeAction, centeredDifference, f.mesh->GetComm()
);
INFO("Complete-action summation error = " << summationError);
INFO(
"Complete-action centered-difference error = "
<< centeredDifferenceError
);
CHECK(preparedOperator.GetCompleteJacobianStatistics().applications == 1);
CHECK(summationError < 5.0e-12);
CHECK(centeredDifferenceError < 2.0e-7);
}
TEST_CASE(
"Prepared Hydrostatic MFEM Adapter Uses Four Block Layout And Reuses "
"Preparation",
tags::barotrope &tags::hydro &tags::integration &tags::jacobian
&tags::mfem_operators &tags::prepared &tags::unit
) {
auto args = test_utils::setup_args();
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
mean_field::operators::PreparedHydrostaticEquilibriumOperator
preparedOperator(f, *f.domainMapperStateless);
const mfem::Vector enthalpy =
gravity_prepared_test_utils::make_deterministic_vector(
f.enthalpyFes->GetTrueVSize(), 0.41
);
const mfem::Vector gravityPotential =
gravity_prepared_test_utils::make_deterministic_vector(
f.gravityPotentialFes->GetTrueVSize(), 0.63
);
const mfem::Vector displacement =
gravity_prepared_test_utils::make_displacement(f, 0.74);
constexpr double bernoulliConstant = 0.38;
preparedOperator.Prepare(
prepared_hydrostatic_complete_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
prepared_hydrostatic_complete_test_utils::make_dependencies(),
prepared_hydrostatic_complete_test_utils::make_rotation()
);
mean_field::operators::PreparedHydrostaticEquilibriumJacobianOperator
adapter(f, preparedOperator);
const auto &layout = adapter.GetLayout();
CHECK(
layout.Offset(
mean_field::operators::HydrostaticJacobianInputBlock::enthalpy
) == 0
);
CHECK(
layout.Offset(
mean_field::operators::HydrostaticJacobianInputBlock::
gravityPotential
) == f.enthalpyFes->GetTrueVSize()
);
CHECK(
layout.Size(
mean_field::operators::HydrostaticJacobianInputBlock::
bernoulliConstant
) == 1
);
CHECK(adapter.Width() == layout.GetTotalSize());
CHECK(adapter.Height() == layout.GetResidualSize());
CHECK(adapter.Height() == f.enthalpyFes->GetTrueVSize());
const mfem::Vector enthalpyVariation =
gravity_prepared_test_utils::make_deterministic_vector(
f.enthalpyFes->GetTrueVSize(), 1.12
);
const mfem::Vector gravityPotentialVariation =
gravity_prepared_test_utils::make_deterministic_vector(
f.gravityPotentialFes->GetTrueVSize(), 1.39
);
const mfem::Vector displacementVariation =
prepared_hydrostatic_complete_test_utils::make_displacement_direction(
f, 1.28, 0.49
);
constexpr double bernoulliConstantVariation = 0.29;
mfem::Vector packedDirection(adapter.Width());
packedDirection = 0.0;
prepared_hydrostatic_complete_test_utils::set_block(
packedDirection, layout,
mean_field::operators::HydrostaticJacobianInputBlock::enthalpy,
enthalpyVariation
);
prepared_hydrostatic_complete_test_utils::set_block(
packedDirection, layout,
mean_field::operators::HydrostaticJacobianInputBlock::gravityPotential,
gravityPotentialVariation
);
prepared_hydrostatic_complete_test_utils::set_block(
packedDirection, layout,
mean_field::operators::HydrostaticJacobianInputBlock::displacement,
displacementVariation
);
packedDirection(layout.Offset(
mean_field::operators::HydrostaticJacobianInputBlock::bernoulliConstant
)) = bernoulliConstantVariation;
mfem::Vector directAction;
mfem::Vector adapterAction;
preparedOperator.ApplyCompleteJacobianAction(
enthalpyVariation, gravityPotentialVariation,
bernoulliConstantVariation, displacementVariation, directAction
);
adapter.Mult(packedDirection, adapterAction);
const double adapterError = gravity_prepared_test_utils::relative_error(
adapterAction, directAction, f.mesh->GetComm()
);
const auto contextStatisticsBefore =
preparedOperator.GetContextPreparationStatistics();
const auto algebraicStatisticsBefore =
preparedOperator.GetAlgebraicJacobianStatistics();
const auto displacementStatisticsBefore =
preparedOperator.GetDisplacementJacobianStatistics();
mfem::Vector secondPackedDirection(packedDirection);
secondPackedDirection *= -0.61;
mfem::Vector secondAdapterAction;
adapter.Mult(secondPackedDirection, secondAdapterAction);
mfem::Vector expectedSecondAction(adapterAction);
expectedSecondAction *= -0.61;
const double adapterLinearityError =
gravity_prepared_test_utils::relative_error(
secondAdapterAction, expectedSecondAction, f.mesh->GetComm()
);
const auto &contextStatisticsAfter =
preparedOperator.GetContextPreparationStatistics();
const auto &algebraicStatisticsAfter =
preparedOperator.GetAlgebraicJacobianStatistics();
const auto &displacementStatisticsAfter =
preparedOperator.GetDisplacementJacobianStatistics();
INFO("MFEM adapter/direct-action error = " << adapterError);
INFO("MFEM adapter linearity error = " << adapterLinearityError);
CHECK(adapterError < 5.0e-12);
CHECK(adapterLinearityError < 5.0e-12);
CHECK(secondAdapterAction.Size() == adapter.Height());
CHECK(contextStatisticsAfter == contextStatisticsBefore);
CHECK(
algebraicStatisticsAfter.preparations ==
algebraicStatisticsBefore.preparations
);
CHECK(
displacementStatisticsAfter.preparations ==
displacementStatisticsBefore.preparations
);
CHECK(preparedOperator.GetCompleteJacobianStatistics().applications == 3);
}

View File

@@ -0,0 +1,423 @@
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
import mean_field;
import test_helpers;
namespace prepared_hydrostatic_displacement_test_utils {
mean_field::operators::context::hydrostatic::
HydrostaticEquilibriumDependencies
make_dependencies() {
return {
.discretization = {.identity = 401, .revision = 2},
.enthalpy = {.identity = 409, .revision = 3},
.gravityPotential = {.identity = 419, .revision = 5},
.displacement = {.identity = 421, .revision = 7},
.rotation = {.identity = 431, .revision = 11},
.bernoulliConstant = {.identity = 433, .revision = 13}
};
}
mean_field::operators::context::hydrostatic::HydrostaticEquilibriumStateView
make_state(
const mfem::Vector &enthalpy,
const mfem::Vector &gravityPotential,
const mfem::Vector &displacement,
const double bernoulliConstant
) {
return {
.enthalpy = enthalpy,
.gravityPotential = gravityPotential,
.displacement = displacement,
.bernoulliConstant = bernoulliConstant
};
}
mfem::Vector make_enthalpy(
const mean_field::fem::FEM &f,
const double phase = 0.29
) {
return gravity_prepared_test_utils::make_deterministic_vector(
f.enthalpyFes->GetTrueVSize(), phase
);
}
mfem::Vector make_gravity_potential(
const mean_field::fem::FEM &f,
const double phase = 0.47
) {
return gravity_prepared_test_utils::make_deterministic_vector(
f.gravityPotentialFes->GetTrueVSize(), phase
);
}
mfem::Vector make_displacement_direction(
const mean_field::fem::FEM &f,
const double firstPhase,
const double secondPhase
) {
mfem::Vector direction =
gravity_prepared_test_utils::make_displacement(f, firstPhase);
const mfem::Vector secondField =
gravity_prepared_test_utils::make_displacement(f, secondPhase);
direction -= secondField;
return direction;
}
mean_field::physics::RigidRotation make_rotation(const double scale = 1.0) {
mfem::Vector angularVelocity(3);
angularVelocity(0) = 0.16 * scale;
angularVelocity(1) = -0.14 * scale;
angularVelocity(2) = 0.46 * scale;
mfem::Vector center(3);
center(0) = 0.034;
center(1) = -0.026;
center(2) = 0.019;
return mean_field::physics::RigidRotation(angularVelocity, center);
}
void centered_displacement_difference(
const mean_field::fem::FEM &f,
const mean_field::physics::RigidRotation &rotation,
const mfem::Vector &enthalpy,
const mfem::Vector &gravityPotential,
const mfem::Vector &baseDisplacement,
const mfem::Vector &displacementVariation,
const double bernoulliConstant,
const double step,
mfem::Vector &difference
) {
mfem::Vector displacementPlus(baseDisplacement);
mfem::Vector displacementMinus(baseDisplacement);
displacementPlus.Add(step, displacementVariation);
displacementMinus.Add(-step, displacementVariation);
mfem::Vector residualPlus;
mfem::Vector residualMinus;
mean_field::operators::kernels::apply_hydrostatic_equilibrium(
f, *f.domainMapperStateless, rotation, enthalpy, gravityPotential,
displacementPlus, bernoulliConstant, residualPlus
);
mean_field::operators::kernels::apply_hydrostatic_equilibrium(
f, *f.domainMapperStateless, rotation, enthalpy, gravityPotential,
displacementMinus, bernoulliConstant, residualMinus
);
difference = residualPlus;
difference -= residualMinus;
difference /= 2.0 * step;
}
double relative_error(
const mfem::Vector &actual,
const mfem::Vector &expected,
const MPI_Comm communicator
) {
return gravity_prepared_test_utils::relative_error(
actual, expected, communicator
);
}
} // namespace prepared_hydrostatic_displacement_test_utils
TEST_CASE(
"Prepared Hydrostatic Displacement Jacobian Matches Centered Differences",
tags::barotrope &tags::hydro &tags::jacobian &tags::prepared &tags::unit
) {
auto args = test_utils::setup_args();
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
mean_field::operators::PreparedHydrostaticEquilibriumOperator
preparedOperator(f, *f.domainMapperStateless);
const mfem::Vector enthalpy =
prepared_hydrostatic_displacement_test_utils::make_enthalpy(f);
const mfem::Vector gravityPotential =
prepared_hydrostatic_displacement_test_utils::make_gravity_potential(f);
const mfem::Vector displacement =
gravity_prepared_test_utils::make_displacement(f, 0.73);
constexpr double bernoulliConstant = 0.39;
const mean_field::physics::RigidRotation rotation =
prepared_hydrostatic_displacement_test_utils::make_rotation(0.9);
const auto dependencies =
prepared_hydrostatic_displacement_test_utils::make_dependencies();
const auto report = preparedOperator.Prepare(
prepared_hydrostatic_displacement_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
dependencies, rotation
);
const mfem::Vector firstVariation =
prepared_hydrostatic_displacement_test_utils::
make_displacement_direction(f, 1.17, 0.31);
const mfem::Vector secondVariation =
prepared_hydrostatic_displacement_test_utils::
make_displacement_direction(f, 1.43, 0.58);
mfem::Vector combinedVariation(firstVariation);
combinedVariation += secondVariation;
mfem::Vector firstAction;
mfem::Vector secondAction;
mfem::Vector combinedAction;
preparedOperator.ApplyDisplacementJacobianAction(
firstVariation, firstAction
);
preparedOperator.ApplyDisplacementJacobianAction(
secondVariation, secondAction
);
preparedOperator.ApplyDisplacementJacobianAction(
combinedVariation, combinedAction
);
constexpr double finiteDifferenceStep = 1.0e-5;
mfem::Vector centeredDifference;
prepared_hydrostatic_displacement_test_utils::
centered_displacement_difference(
f, rotation, enthalpy, gravityPotential, displacement,
firstVariation, bernoulliConstant, finiteDifferenceStep,
centeredDifference
);
mfem::Vector sumOfActions(firstAction);
sumOfActions += secondAction;
const double centeredDifferenceError =
prepared_hydrostatic_displacement_test_utils::relative_error(
firstAction, centeredDifference, f.mesh->GetComm()
);
const double linearityError =
prepared_hydrostatic_displacement_test_utils::relative_error(
combinedAction, sumOfActions, f.mesh->GetComm()
);
INFO(
"Prepared displacement centered-difference error = "
<< centeredDifferenceError
);
INFO("Prepared displacement linearity error = " << linearityError);
const auto &statistics =
preparedOperator.GetDisplacementJacobianStatistics();
CHECK(report.preparedDisplacementJacobianData);
CHECK(statistics.preparations == 1);
CHECK(statistics.applications == 3);
CHECK(centeredDifferenceError < 2.0e-7);
CHECK(linearityError < 5.0e-12);
}
TEST_CASE(
"Prepared Hydrostatic Displacement Jacobian Reuses And Refreshes Frozen "
"Data",
tags::barotrope &tags::hydro &tags::jacobian &tags::prepared &tags::unit
) {
auto args = test_utils::setup_args();
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
mean_field::operators::PreparedHydrostaticEquilibriumOperator
preparedOperator(f, *f.domainMapperStateless);
const mfem::Vector enthalpy =
prepared_hydrostatic_displacement_test_utils::make_enthalpy(f, 0.37);
const mfem::Vector gravityPotential =
prepared_hydrostatic_displacement_test_utils::make_gravity_potential(
f, 0.53
);
mfem::Vector displacement =
gravity_prepared_test_utils::make_displacement(f, 0.42);
constexpr double bernoulliConstant = 0.36;
mean_field::physics::RigidRotation rotation =
prepared_hydrostatic_displacement_test_utils::make_rotation(0.75);
auto dependencies =
prepared_hydrostatic_displacement_test_utils::make_dependencies();
preparedOperator.Prepare(
prepared_hydrostatic_displacement_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
dependencies, rotation
);
const mfem::Vector displacementVariation =
prepared_hydrostatic_displacement_test_utils::
make_displacement_direction(f, 1.09, 0.27);
const mfem::Vector secondVariation =
prepared_hydrostatic_displacement_test_utils::
make_displacement_direction(f, 1.36, 0.64);
mfem::Vector initialAction;
mfem::Vector secondDirectionAction;
preparedOperator.ApplyDisplacementJacobianAction(
displacementVariation, initialAction
);
preparedOperator.ApplyDisplacementJacobianAction(
secondVariation, secondDirectionAction
);
CHECK(
preparedOperator.GetDisplacementJacobianStatistics().preparations == 1
);
rotation =
prepared_hydrostatic_displacement_test_utils::make_rotation(1.45);
++dependencies.rotation.revision;
const auto rotationReport = preparedOperator.Prepare(
prepared_hydrostatic_displacement_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
dependencies, rotation
);
mfem::Vector rotationUpdatedAction;
preparedOperator.ApplyDisplacementJacobianAction(
displacementVariation, rotationUpdatedAction
);
constexpr double finiteDifferenceStep = 1.0e-5;
mfem::Vector rotationReference;
prepared_hydrostatic_displacement_test_utils::
centered_displacement_difference(
f, rotation, enthalpy, gravityPotential, displacement,
displacementVariation, bernoulliConstant, finiteDifferenceStep,
rotationReference
);
const double rotationReferenceError =
prepared_hydrostatic_displacement_test_utils::relative_error(
rotationUpdatedAction, rotationReference, f.mesh->GetComm()
);
const double rotationEffect =
prepared_hydrostatic_displacement_test_utils::relative_error(
rotationUpdatedAction, initialAction, f.mesh->GetComm()
);
CHECK_FALSE(rotationReport.contextReport.preparedGeometryState);
CHECK(rotationReport.contextReport.preparedRotationDependencies);
CHECK(rotationReport.contextReport.preparedBaseState);
CHECK(rotationReport.updatedRotation);
CHECK(rotationReport.preparedDisplacementJacobianData);
CHECK_FALSE(rotationReport.preparedAlgebraicJacobianBlocks);
CHECK(rotationReferenceError < 2.0e-7);
CHECK(rotationEffect > 1.0e-8);
displacement = gravity_prepared_test_utils::make_displacement(f, 0.91);
++dependencies.displacement.revision;
const auto geometryReport = preparedOperator.Prepare(
prepared_hydrostatic_displacement_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
dependencies, rotation
);
mfem::Vector geometryUpdatedAction;
preparedOperator.ApplyDisplacementJacobianAction(
displacementVariation, geometryUpdatedAction
);
mfem::Vector geometryReference;
prepared_hydrostatic_displacement_test_utils::
centered_displacement_difference(
f, rotation, enthalpy, gravityPotential, displacement,
displacementVariation, bernoulliConstant, finiteDifferenceStep,
geometryReference
);
const double geometryReferenceError =
prepared_hydrostatic_displacement_test_utils::relative_error(
geometryUpdatedAction, geometryReference, f.mesh->GetComm()
);
const double geometryEffect =
prepared_hydrostatic_displacement_test_utils::relative_error(
geometryUpdatedAction, rotationUpdatedAction, f.mesh->GetComm()
);
INFO(
"Rotation-updated displacement Jacobian error = "
<< rotationReferenceError
);
INFO(
"Displacement Jacobian change after rotation update = "
<< rotationEffect
);
INFO(
"Geometry-updated displacement Jacobian error = "
<< geometryReferenceError
);
INFO(
"Displacement Jacobian change after geometry update = "
<< geometryEffect
);
const auto &contextStatistics =
preparedOperator.GetContextPreparationStatistics();
const auto &displacementStatistics =
preparedOperator.GetDisplacementJacobianStatistics();
CHECK(geometryReport.contextReport.preparedGeometryState);
CHECK(geometryReport.contextReport.preparedRotationDependencies);
CHECK(geometryReport.contextReport.preparedBaseState);
CHECK_FALSE(geometryReport.updatedRotation);
CHECK(geometryReport.preparedAlgebraicJacobianBlocks);
CHECK(geometryReport.preparedDisplacementJacobianData);
CHECK(contextStatistics.staticPreparations == 1);
CHECK(contextStatistics.geometryPreparations == 2);
CHECK(contextStatistics.rotationPreparations == 3);
CHECK(contextStatistics.baseStatePreparations == 3);
CHECK(displacementStatistics.preparations == 3);
CHECK(displacementStatistics.applications == 4);
CHECK(geometryReferenceError < 2.0e-7);
CHECK(geometryEffect > 1.0e-8);
}

View File

@@ -0,0 +1,472 @@
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
import mean_field;
import test_helpers;
namespace prepared_hydrostatic_jacobian_test_utils {
mean_field::operators::context::hydrostatic::
HydrostaticEquilibriumDependencies
make_dependencies() {
return {
.discretization = {.identity = 307, .revision = 2},
.enthalpy = {.identity = 311, .revision = 3},
.gravityPotential = {.identity = 313, .revision = 5},
.displacement = {.identity = 317, .revision = 7},
.rotation = {.identity = 331, .revision = 11},
.bernoulliConstant = {.identity = 337, .revision = 13}
};
}
mean_field::operators::context::hydrostatic::HydrostaticEquilibriumStateView
make_state(
const mfem::Vector &enthalpy,
const mfem::Vector &gravityPotential,
const mfem::Vector &displacement,
const double bernoulliConstant
) {
return {
.enthalpy = enthalpy,
.gravityPotential = gravityPotential,
.displacement = displacement,
.bernoulliConstant = bernoulliConstant
};
}
mfem::Vector make_enthalpy(
const mean_field::fem::FEM &f,
const double phase = 0.23
) {
return gravity_prepared_test_utils::make_deterministic_vector(
f.enthalpyFes->GetTrueVSize(), phase
);
}
mfem::Vector make_gravity_potential(
const mean_field::fem::FEM &f,
const double phase = 0.41
) {
return gravity_prepared_test_utils::make_deterministic_vector(
f.gravityPotentialFes->GetTrueVSize(), phase
);
}
mean_field::physics::RigidRotation make_rotation(const double scale = 1.0) {
mfem::Vector angularVelocity(3);
angularVelocity(0) = 0.13 * scale;
angularVelocity(1) = -0.19 * scale;
angularVelocity(2) = 0.47 * scale;
mfem::Vector center(3);
center(0) = 0.031;
center(1) = -0.023;
center(2) = 0.017;
return mean_field::physics::RigidRotation(angularVelocity, center);
}
void centered_residual_difference(
const mean_field::fem::FEM &f,
const mean_field::physics::RigidRotation &rotation,
const mfem::Vector &enthalpyPlus,
const mfem::Vector &enthalpyMinus,
const mfem::Vector &gravityPotentialPlus,
const mfem::Vector &gravityPotentialMinus,
const mfem::Vector &displacement,
const double bernoulliConstantPlus,
const double bernoulliConstantMinus,
mfem::Vector &difference
) {
mfem::Vector residualPlus;
mfem::Vector residualMinus;
mean_field::operators::kernels::apply_hydrostatic_equilibrium(
f, *f.domainMapperStateless, rotation, enthalpyPlus,
gravityPotentialPlus, displacement, bernoulliConstantPlus,
residualPlus
);
mean_field::operators::kernels::apply_hydrostatic_equilibrium(
f, *f.domainMapperStateless, rotation, enthalpyMinus,
gravityPotentialMinus, displacement, bernoulliConstantMinus,
residualMinus
);
// The two states are separated by one complete variation:
// x_+ = x + 0.5 dx and x_- = x - 0.5 dx.
difference = residualPlus;
difference -= residualMinus;
}
double relative_error(
const mfem::Vector &actual,
const mfem::Vector &expected,
MPI_Comm communicator
) {
return gravity_prepared_test_utils::relative_error(
actual, expected, communicator
);
}
} // namespace prepared_hydrostatic_jacobian_test_utils
TEST_CASE(
"Prepared Hydrostatic Algebraic Jacobian Matches Centered Residual "
"Differences",
tags::barotrope &tags::hydro &tags::jacobian &tags::prepared &tags::unit
) {
auto args = test_utils::setup_args();
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
mean_field::operators::PreparedHydrostaticEquilibriumOperator
preparedOperator(f, *f.domainMapperStateless);
const mfem::Vector enthalpy =
prepared_hydrostatic_jacobian_test_utils::make_enthalpy(f);
const mfem::Vector gravityPotential =
prepared_hydrostatic_jacobian_test_utils::make_gravity_potential(f);
const mfem::Vector displacement =
gravity_prepared_test_utils::make_displacement(f, 0.73);
constexpr double bernoulliConstant = 0.39;
const mean_field::physics::RigidRotation rotation =
prepared_hydrostatic_jacobian_test_utils::make_rotation();
const auto report = preparedOperator.Prepare(
prepared_hydrostatic_jacobian_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
prepared_hydrostatic_jacobian_test_utils::make_dependencies(), rotation
);
const mfem::Vector enthalpyVariation =
prepared_hydrostatic_jacobian_test_utils::make_enthalpy(f, 0.71);
const mfem::Vector gravityPotentialVariation =
prepared_hydrostatic_jacobian_test_utils::make_gravity_potential(
f, 0.83
);
constexpr double bernoulliConstantVariation = -0.31;
mfem::Vector enthalpyAction;
mfem::Vector gravityPotentialAction;
mfem::Vector bernoulliConstantAction;
mfem::Vector combinedAction;
preparedOperator.ApplyEnthalpyJacobianAction(
enthalpyVariation, enthalpyAction
);
preparedOperator.ApplyGravityPotentialJacobianAction(
gravityPotentialVariation, gravityPotentialAction
);
preparedOperator.ApplyBernoulliConstantJacobianAction(
bernoulliConstantVariation, bernoulliConstantAction
);
preparedOperator.ApplyAlgebraicJacobianAction(
enthalpyVariation, gravityPotentialVariation,
bernoulliConstantVariation, combinedAction
);
mfem::Vector enthalpyPlus(enthalpy);
mfem::Vector enthalpyMinus(enthalpy);
mfem::Vector gravityPotentialPlus(gravityPotential);
mfem::Vector gravityPotentialMinus(gravityPotential);
enthalpyPlus.Add(0.5, enthalpyVariation);
enthalpyMinus.Add(-0.5, enthalpyVariation);
mfem::Vector enthalpyReference;
prepared_hydrostatic_jacobian_test_utils::centered_residual_difference(
f, rotation, enthalpyPlus, enthalpyMinus, gravityPotential,
gravityPotential, displacement, bernoulliConstant, bernoulliConstant,
enthalpyReference
);
enthalpyPlus = enthalpy;
enthalpyMinus = enthalpy;
gravityPotentialPlus.Add(0.5, gravityPotentialVariation);
gravityPotentialMinus.Add(-0.5, gravityPotentialVariation);
mfem::Vector gravityPotentialReference;
prepared_hydrostatic_jacobian_test_utils::centered_residual_difference(
f, rotation, enthalpy, enthalpy, gravityPotentialPlus,
gravityPotentialMinus, displacement, bernoulliConstant,
bernoulliConstant, gravityPotentialReference
);
gravityPotentialPlus = gravityPotential;
gravityPotentialMinus = gravityPotential;
mfem::Vector bernoulliConstantReference;
prepared_hydrostatic_jacobian_test_utils::centered_residual_difference(
f, rotation, enthalpy, enthalpy, gravityPotential, gravityPotential,
displacement, bernoulliConstant + 0.5 * bernoulliConstantVariation,
bernoulliConstant - 0.5 * bernoulliConstantVariation,
bernoulliConstantReference
);
enthalpyPlus.Add(0.5, enthalpyVariation);
enthalpyMinus.Add(-0.5, enthalpyVariation);
gravityPotentialPlus.Add(0.5, gravityPotentialVariation);
gravityPotentialMinus.Add(-0.5, gravityPotentialVariation);
mfem::Vector combinedReference;
prepared_hydrostatic_jacobian_test_utils::centered_residual_difference(
f, rotation, enthalpyPlus, enthalpyMinus, gravityPotentialPlus,
gravityPotentialMinus, displacement,
bernoulliConstant + 0.5 * bernoulliConstantVariation,
bernoulliConstant - 0.5 * bernoulliConstantVariation, combinedReference
);
mfem::Vector sumOfBlocks(enthalpyAction);
sumOfBlocks += gravityPotentialAction;
sumOfBlocks += bernoulliConstantAction;
const double enthalpyError =
prepared_hydrostatic_jacobian_test_utils::relative_error(
enthalpyAction, enthalpyReference, f.mesh->GetComm()
);
const double gravityPotentialError =
prepared_hydrostatic_jacobian_test_utils::relative_error(
gravityPotentialAction, gravityPotentialReference, f.mesh->GetComm()
);
const double bernoulliConstantError =
prepared_hydrostatic_jacobian_test_utils::relative_error(
bernoulliConstantAction, bernoulliConstantReference,
f.mesh->GetComm()
);
const double combinedError =
prepared_hydrostatic_jacobian_test_utils::relative_error(
combinedAction, combinedReference, f.mesh->GetComm()
);
const double blockSumError =
prepared_hydrostatic_jacobian_test_utils::relative_error(
combinedAction, sumOfBlocks, f.mesh->GetComm()
);
INFO("Enthalpy block error = " << enthalpyError);
INFO("Gravity-potential block error = " << gravityPotentialError);
INFO("Bernoulli-constant block error = " << bernoulliConstantError);
INFO("Combined algebraic action error = " << combinedError);
INFO("Combined-versus-summed-block error = " << blockSumError);
const auto &statistics = preparedOperator.GetAlgebraicJacobianStatistics();
CHECK(report.preparedAlgebraicJacobianBlocks);
CHECK(statistics.preparations == 1);
CHECK(statistics.enthalpyApplications == 1);
CHECK(statistics.gravityPotentialApplications == 1);
CHECK(statistics.bernoulliConstantApplications == 1);
CHECK(statistics.combinedApplications == 1);
CHECK(enthalpyError < 5.0e-12);
CHECK(gravityPotentialError < 5.0e-12);
CHECK(bernoulliConstantError < 5.0e-12);
CHECK(combinedError < 5.0e-12);
CHECK(blockSumError < 5.0e-13);
}
TEST_CASE(
"Prepared Hydrostatic Algebraic Jacobian Reuses And Rebuilds Only With "
"Geometry",
tags::barotrope &tags::hydro &tags::jacobian &tags::prepared &tags::unit
) {
auto args = test_utils::setup_args();
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
mean_field::operators::PreparedHydrostaticEquilibriumOperator
preparedOperator(f, *f.domainMapperStateless);
mfem::Vector enthalpy =
prepared_hydrostatic_jacobian_test_utils::make_enthalpy(f);
mfem::Vector gravityPotential =
prepared_hydrostatic_jacobian_test_utils::make_gravity_potential(f);
mfem::Vector displacement =
gravity_prepared_test_utils::make_displacement(f, 0.42);
double bernoulliConstant = 0.37;
mean_field::physics::RigidRotation rotation =
prepared_hydrostatic_jacobian_test_utils::make_rotation(0.8);
auto dependencies =
prepared_hydrostatic_jacobian_test_utils::make_dependencies();
preparedOperator.Prepare(
prepared_hydrostatic_jacobian_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
dependencies, rotation
);
const mfem::Vector enthalpyVariation =
prepared_hydrostatic_jacobian_test_utils::make_enthalpy(f, 0.67);
const mfem::Vector gravityPotentialVariation =
prepared_hydrostatic_jacobian_test_utils::make_gravity_potential(
f, 0.79
);
constexpr double bernoulliConstantVariation = 0.28;
mfem::Vector initialAction;
preparedOperator.ApplyAlgebraicJacobianAction(
enthalpyVariation, gravityPotentialVariation,
bernoulliConstantVariation, initialAction
);
enthalpy = prepared_hydrostatic_jacobian_test_utils::make_enthalpy(f, 1.13);
gravityPotential =
prepared_hydrostatic_jacobian_test_utils::make_gravity_potential(
f, 1.31
);
bernoulliConstant = 0.62;
rotation = prepared_hydrostatic_jacobian_test_utils::make_rotation(1.4);
++dependencies.enthalpy.revision;
++dependencies.gravityPotential.revision;
++dependencies.bernoulliConstant.revision;
++dependencies.rotation.revision;
const auto baseStateReport = preparedOperator.Prepare(
prepared_hydrostatic_jacobian_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
dependencies, rotation
);
mfem::Vector baseStateChangedAction;
preparedOperator.ApplyAlgebraicJacobianAction(
enthalpyVariation, gravityPotentialVariation,
bernoulliConstantVariation, baseStateChangedAction
);
CHECK_FALSE(baseStateReport.contextReport.preparedGeometryState);
CHECK(baseStateReport.contextReport.preparedRotationDependencies);
CHECK(baseStateReport.contextReport.preparedBaseState);
CHECK_FALSE(baseStateReport.preparedAlgebraicJacobianBlocks);
CHECK(preparedOperator.GetAlgebraicJacobianStatistics().preparations == 1);
CHECK(
prepared_hydrostatic_jacobian_test_utils::relative_error(
baseStateChangedAction, initialAction, f.mesh->GetComm()
) == 0.0
);
const mfem::Vector secondEnthalpyVariation =
prepared_hydrostatic_jacobian_test_utils::make_enthalpy(f, 1.57);
const mfem::Vector secondGravityPotentialVariation =
prepared_hydrostatic_jacobian_test_utils::make_gravity_potential(
f, 1.73
);
mfem::Vector secondDirectionAction;
preparedOperator.ApplyAlgebraicJacobianAction(
secondEnthalpyVariation, secondGravityPotentialVariation, -0.19,
secondDirectionAction
);
CHECK(preparedOperator.GetAlgebraicJacobianStatistics().preparations == 1);
displacement = gravity_prepared_test_utils::make_displacement(f, 0.73);
++dependencies.displacement.revision;
const auto geometryReport = preparedOperator.Prepare(
prepared_hydrostatic_jacobian_test_utils::make_state(
enthalpy, gravityPotential, displacement, bernoulliConstant
),
dependencies, rotation
);
mfem::Vector geometryChangedAction;
preparedOperator.ApplyAlgebraicJacobianAction(
enthalpyVariation, gravityPotentialVariation,
bernoulliConstantVariation, geometryChangedAction
);
mfem::Vector enthalpyPlus(enthalpy);
mfem::Vector enthalpyMinus(enthalpy);
mfem::Vector gravityPotentialPlus(gravityPotential);
mfem::Vector gravityPotentialMinus(gravityPotential);
enthalpyPlus.Add(0.5, enthalpyVariation);
enthalpyMinus.Add(-0.5, enthalpyVariation);
gravityPotentialPlus.Add(0.5, gravityPotentialVariation);
gravityPotentialMinus.Add(-0.5, gravityPotentialVariation);
mfem::Vector geometryReference;
prepared_hydrostatic_jacobian_test_utils::centered_residual_difference(
f, rotation, enthalpyPlus, enthalpyMinus, gravityPotentialPlus,
gravityPotentialMinus, displacement,
bernoulliConstant + 0.5 * bernoulliConstantVariation,
bernoulliConstant - 0.5 * bernoulliConstantVariation, geometryReference
);
const double geometryReferenceError =
prepared_hydrostatic_jacobian_test_utils::relative_error(
geometryChangedAction, geometryReference, f.mesh->GetComm()
);
const double geometryEffect =
prepared_hydrostatic_jacobian_test_utils::relative_error(
geometryChangedAction, initialAction, f.mesh->GetComm()
);
INFO(
"Geometry-updated algebraic Jacobian error = " << geometryReferenceError
);
INFO(
"Algebraic Jacobian change after deformation update = "
<< geometryEffect
);
const auto &statistics = preparedOperator.GetAlgebraicJacobianStatistics();
CHECK(geometryReport.contextReport.preparedGeometryState);
CHECK(geometryReport.preparedAlgebraicJacobianBlocks);
CHECK(statistics.preparations == 2);
CHECK(statistics.enthalpyApplications == 0);
CHECK(statistics.gravityPotentialApplications == 0);
CHECK(statistics.bernoulliConstantApplications == 0);
CHECK(statistics.combinedApplications == 4);
CHECK(geometryReferenceError < 5.0e-12);
CHECK(geometryEffect > 1.0e-8);
}

144
tests/physics/barotrope.cpp Normal file
View File

@@ -0,0 +1,144 @@
#include <array>
#include <cmath>
#include <limits>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
import mean_field;
import test_helpers;
TEST_CASE(
"Polytropic Barotrope Satisfies Its Analytic Identities",
tags::hydro &tags::unit &tags::barotrope
) {
constexpr double polytropic_index = 3.0;
constexpr double polytropic_constant = 1.5;
const mean_field::physics::PolytropicBarotrope barotrope(
polytropic_index, polytropic_constant
);
const std::array<double, 5> densities{1.0e-6, 1.0e-3, 0.1, 0.7, 2.0};
for (const double density : densities) {
const double pressure = barotrope.pressure_from_density(density);
const double enthalpy = barotrope.enthalpy_from_density(density);
const double reconstructed_density =
barotrope.density_from_enthalpy(enthalpy);
const double reconstructed_pressure =
barotrope.pressure_from_enthalpy(enthalpy);
CHECK_THAT(
reconstructed_density, Catch::Matchers::WithinRel(density, 2.0e-14)
);
CHECK_THAT(
reconstructed_pressure,
Catch::Matchers::WithinRel(pressure, 2.0e-14)
);
CHECK_THAT(
pressure, Catch::Matchers::WithinRel(
density * enthalpy / (polytropic_index + 1.0), 2.0e-14
)
);
CHECK_THAT(
barotrope.pressure_derivative_from_enthalpy(enthalpy),
Catch::Matchers::WithinRel(density, 2.0e-14)
);
CHECK_THAT(
barotrope.pressure_derivative_from_density(density),
Catch::Matchers::WithinRel(enthalpy / polytropic_index, 2.0e-14)
);
}
}
TEST_CASE(
"Polytropic Barotrope Derivatives Match Centered Differences",
tags::hydro &tags::jacobian &tags::unit &tags::barotrope
) {
const mean_field::physics::PolytropicBarotrope barotrope(3.0, 1.5);
const std::array<double, 4> enthalpies{0.05, 0.2, 0.7, 1.4};
for (const double enthalpy : enthalpies) {
const double step = 1.0e-6 * std::max(1.0, enthalpy);
const double density_difference =
(barotrope.density_from_enthalpy(enthalpy + step) -
barotrope.density_from_enthalpy(enthalpy - step)) /
(2.0 * step);
const double pressure_difference =
(barotrope.pressure_from_enthalpy(enthalpy + step) -
barotrope.pressure_from_enthalpy(enthalpy - step)) /
(2.0 * step);
CHECK_THAT(
density_difference,
Catch::Matchers::WithinRel(
barotrope.density_derivative_from_enthalpy(enthalpy), 5.0e-10
)
);
CHECK_THAT(
pressure_difference,
Catch::Matchers::WithinRel(
barotrope.pressure_derivative_from_enthalpy(enthalpy), 5.0e-10
)
);
}
}
TEST_CASE(
"Polytropic Barotrope Has An Exact Zero Density Surface",
tags::hydro &tags::unit &tags::barotrope
) {
const mean_field::physics::PolytropicBarotrope barotrope(3.0, 1.5);
CHECK(barotrope.density_from_enthalpy(-1.0) == 0.0);
CHECK(barotrope.density_from_enthalpy(0.0) == 0.0);
CHECK(barotrope.pressure_from_enthalpy(-1.0) == 0.0);
CHECK(barotrope.pressure_from_enthalpy(0.0) == 0.0);
CHECK(barotrope.density_derivative_from_enthalpy(-1.0) == 0.0);
CHECK(barotrope.density_derivative_from_enthalpy(0.0) == 0.0);
CHECK(barotrope.pressure_derivative_from_enthalpy(0.0) == 0.0);
}
TEST_CASE(
"Polytropic Barotrope Rejects Invalid Material Parameters",
tags::hydro &tags::unit
) {
CHECK_THROWS_AS(
mean_field::physics::PolytropicBarotrope(0.5, 1.0),
std::invalid_argument
);
CHECK_THROWS_AS(
mean_field::physics::PolytropicBarotrope(3.0, 0.0),
std::invalid_argument
);
CHECK_THROWS_AS(
mean_field::physics::PolytropicBarotrope(
std::numeric_limits<double>::infinity(), 1.0
),
std::invalid_argument
);
const mean_field::physics::PolytropicBarotrope barotrope(3.0, 1.0);
CHECK_THROWS_AS(barotrope.pressure_from_density(-1.0), std::domain_error);
CHECK_THROWS_AS(barotrope.enthalpy_from_density(-1.0), std::domain_error);
}

View File

@@ -0,0 +1,699 @@
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <stdexcept>
#include <utility>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <mfem.hpp>
import mean_field;
import test_helpers;
namespace polytropic_barotrope_test_utils {
template <typename Function>
double centered_derivative(
Function &&function,
const double position,
const double step
) {
return (function(position + step) - function(position - step)) /
(2.0 * step);
}
template <typename Integrand>
double integrate_cube(
const mfem::IntegrationRule &integrationRule,
Integrand &&integrand
) {
double integral = 0.0;
for (int pointIndex = 0; pointIndex < integrationRule.GetNPoints();
++pointIndex) {
const mfem::IntegrationPoint &integrationPoint =
integrationRule.IntPoint(pointIndex);
integral += integrationPoint.weight * integrand(integrationPoint);
}
return integral;
}
} // namespace polytropic_barotrope_test_utils
TEST_CASE(
"Polytropic Barotrope Satisfies Its Thermodynamic Identities",
tags::barotrope &tags::physics &tags::unit
) {
constexpr std::array<double, 3> polytropicIndices{1.0, 1.5, 3.0};
constexpr std::array<double, 4> densities{1.0e-4, 0.02, 0.37, 2.4};
constexpr double polytropicConstant = 0.73;
for (const double polytropicIndex : polytropicIndices) {
DYNAMIC_SECTION("polytropic index n = " << polytropicIndex) {
const mean_field::physics::PolytropicBarotrope barotrope(
polytropicIndex, polytropicConstant
);
const double expectedEnthalpyScale =
(polytropicIndex + 1.0) * polytropicConstant;
CHECK(barotrope.polytropic_index() == polytropicIndex);
CHECK(barotrope.polytropic_constant() == polytropicConstant);
CHECK(barotrope.enthalpy_scale() == expectedEnthalpyScale);
for (const double density : densities) {
CAPTURE(polytropicIndex, polytropicConstant, density);
const double expectedPressure =
polytropicConstant *
std::pow(density, 1.0 + 1.0 / polytropicIndex);
const double expectedEnthalpy =
expectedEnthalpyScale *
std::pow(density, 1.0 / polytropicIndex);
const double pressureFromDensity =
barotrope.pressure_from_density(density);
const double enthalpyFromDensity =
barotrope.enthalpy_from_density(density);
const double recoveredDensity =
barotrope.density_from_enthalpy(enthalpyFromDensity);
const double pressureFromEnthalpy =
barotrope.pressure_from_enthalpy(enthalpyFromDensity);
CHECK_THAT(
pressureFromDensity,
Catch::Matchers::WithinRel(expectedPressure, 2.0e-13)
);
CHECK_THAT(
enthalpyFromDensity,
Catch::Matchers::WithinRel(expectedEnthalpy, 2.0e-13)
);
CHECK_THAT(
recoveredDensity,
Catch::Matchers::WithinRel(density, 5.0e-13)
);
CHECK_THAT(
pressureFromEnthalpy,
Catch::Matchers::WithinRel(expectedPressure, 5.0e-13)
);
/*
* Polytropic identity:
*
* P = rho h / (n + 1).
*/
CHECK_THAT(
pressureFromEnthalpy,
Catch::Matchers::WithinRel(
density * enthalpyFromDensity / (polytropicIndex + 1.0),
5.0e-13
)
);
/*
* Polytropic identity:
*
* dP / dh = rho.
*
* The implementation should return the same
* value as density_from_enthalpy().
*/
CHECK(
barotrope.pressure_derivative_from_enthalpy(
enthalpyFromDensity
) == barotrope.density_from_enthalpy(enthalpyFromDensity)
);
/*
* Since
*
* h = (n + 1) K rho^(1/n),
*
* it follows that
*
* dP / d rho = h / n.
*/
CHECK_THAT(
barotrope.pressure_derivative_from_density(density),
Catch::Matchers::WithinRel(
enthalpyFromDensity / polytropicIndex, 5.0e-13
)
);
}
}
}
}
TEST_CASE(
"Polytropic Barotrope Pressure Derivatives Match Centered Differences",
tags::barotrope &tags::physics &tags::unit &tags::jacobian &tags::pressure
) {
constexpr std::array<double, 3> polytropicIndices{1.0, 1.5, 3.0};
constexpr std::array<double, 3> positiveValues{0.2, 0.73, 1.8};
constexpr double polytropicConstant = 0.61;
for (const double polytropicIndex : polytropicIndices) {
const mean_field::physics::PolytropicBarotrope barotrope(
polytropicIndex, polytropicConstant
);
DYNAMIC_SECTION("polytropic index n = " << polytropicIndex) {
for (const double enthalpy : positiveValues) {
const double step = 2.0e-6 * std::max(1.0, std::abs(enthalpy));
const double numericalDerivative =
polytropic_barotrope_test_utils::centered_derivative(
[&barotrope](const double perturbedEnthalpy) {
return barotrope.pressure_from_enthalpy(
perturbedEnthalpy
);
},
enthalpy, step
);
const double analyticDerivative =
barotrope.pressure_derivative_from_enthalpy(enthalpy);
CAPTURE(
polytropicIndex, enthalpy, step, numericalDerivative,
analyticDerivative
);
CHECK_THAT(
numericalDerivative,
Catch::Matchers::WithinRel(analyticDerivative, 5.0e-8)
);
}
for (const double density : positiveValues) {
const double step = 2.0e-6 * std::max(1.0, std::abs(density));
const double numericalDerivative =
polytropic_barotrope_test_utils::centered_derivative(
[&barotrope](const double perturbedDensity) {
return barotrope.pressure_from_density(
perturbedDensity
);
},
density, step
);
const double analyticDerivative =
barotrope.pressure_derivative_from_density(density);
CAPTURE(
polytropicIndex, density, step, numericalDerivative,
analyticDerivative
);
CHECK_THAT(
numericalDerivative,
Catch::Matchers::WithinRel(analyticDerivative, 5.0e-8)
);
}
}
}
}
TEST_CASE(
"Polytropic Barotrope Density Derivative Matches Centered Differences",
tags::barotrope &tags::physics &tags::unit &tags::jacobian &tags::pressure
) {
constexpr std::array<double, 3> polytropicIndices{1.0, 1.5, 3.0};
constexpr std::array<double, 3> enthalpies{0.2, 0.73, 1.8};
constexpr double polytropicConstant = 0.61;
for (const double polytropicIndex : polytropicIndices) {
const mean_field::physics::PolytropicBarotrope barotrope(
polytropicIndex, polytropicConstant
);
DYNAMIC_SECTION("polytropic index n = " << polytropicIndex) {
for (const double enthalpy : enthalpies) {
const double step = 2.0e-6 * std::max(1.0, std::abs(enthalpy));
const double numericalDerivative =
polytropic_barotrope_test_utils::centered_derivative(
[&barotrope](const double perturbedEnthalpy) {
return barotrope.density_from_enthalpy(
perturbedEnthalpy
);
},
enthalpy, step
);
const double analyticDerivative =
barotrope.density_derivative_from_enthalpy(enthalpy);
CAPTURE(
polytropicIndex, enthalpy, step, numericalDerivative,
analyticDerivative
);
CHECK_THAT(
numericalDerivative,
Catch::Matchers::WithinRel(analyticDerivative, 5.0e-8)
);
}
}
}
}
TEST_CASE(
"Polytropic Barotrope Defines Consistent Surface And Exterior Behavior",
tags::barotrope &tags::physics &tags::unit &tags::pressure
) {
constexpr std::array<double, 3> polytropicIndices{1.0, 1.5, 3.0};
constexpr double polytropicConstant = 0.47;
constexpr double exteriorEnthalpy = -0.3;
for (const double polytropicIndex : polytropicIndices) {
const mean_field::physics::PolytropicBarotrope barotrope(
polytropicIndex, polytropicConstant
);
DYNAMIC_SECTION("polytropic index n = " << polytropicIndex) {
/*
* Exact surface values.
*/
CHECK(barotrope.density_from_enthalpy(0.0) == 0.0);
CHECK(barotrope.pressure_from_enthalpy(0.0) == 0.0);
CHECK(barotrope.pressure_derivative_from_enthalpy(0.0) == 0.0);
CHECK(barotrope.pressure_from_density(0.0) == 0.0);
CHECK(barotrope.enthalpy_from_density(0.0) == 0.0);
CHECK(barotrope.pressure_derivative_from_density(0.0) == 0.0);
/*
* Positive-part extension into h < 0.
*/
CHECK(barotrope.density_from_enthalpy(exteriorEnthalpy) == 0.0);
CHECK(barotrope.pressure_from_enthalpy(exteriorEnthalpy) == 0.0);
CHECK(
barotrope.density_derivative_from_enthalpy(exteriorEnthalpy) ==
0.0
);
CHECK(
barotrope.pressure_derivative_from_enthalpy(exteriorEnthalpy) ==
0.0
);
/*
* At h = 0, rho(h) has a nonzero right
* derivative only for n = 1.
*/
const double expectedSurfaceDensityDerivative =
polytropicIndex == 1.0 ? 1.0 / barotrope.enthalpy_scale() : 0.0;
CHECK(
barotrope.density_derivative_from_enthalpy(0.0) ==
expectedSurfaceDensityDerivative
);
}
}
}
TEST_CASE(
"Polytropic Barotrope Rejects Invalid Physical Inputs",
tags::barotrope &tags::physics &tags::unit &tags::pressure
) {
CHECK_THROWS_AS(
mean_field::physics::PolytropicBarotrope(0.999, 1.0),
std::invalid_argument
);
CHECK_THROWS_AS(
mean_field::physics::PolytropicBarotrope(
std::numeric_limits<double>::infinity(), 1.0
),
std::invalid_argument
);
CHECK_THROWS_AS(
mean_field::physics::PolytropicBarotrope(3.0, 0.0),
std::invalid_argument
);
CHECK_THROWS_AS(
mean_field::physics::PolytropicBarotrope(3.0, -1.0),
std::invalid_argument
);
const mean_field::physics::PolytropicBarotrope barotrope(3.0, 0.75);
CHECK_THROWS_AS(barotrope.pressure_from_density(-0.1), std::domain_error);
CHECK_THROWS_AS(barotrope.enthalpy_from_density(-0.1), std::domain_error);
CHECK_THROWS_AS(
barotrope.pressure_derivative_from_density(-0.1), std::domain_error
);
constexpr std::array<double, 3> nonfiniteValues{
std::numeric_limits<double>::infinity(),
-std::numeric_limits<double>::infinity(),
std::numeric_limits<double>::quiet_NaN()
};
for (const double nonfiniteValue : nonfiniteValues) {
CAPTURE(nonfiniteValue);
CHECK_THROWS_AS(
barotrope.density_from_enthalpy(nonfiniteValue), std::domain_error
);
CHECK_THROWS_AS(
barotrope.pressure_from_enthalpy(nonfiniteValue), std::domain_error
);
CHECK_THROWS_AS(
barotrope.density_derivative_from_enthalpy(nonfiniteValue),
std::domain_error
);
CHECK_THROWS_AS(
barotrope.pressure_derivative_from_enthalpy(nonfiniteValue),
std::domain_error
);
}
}
TEST_CASE(
"Pressure Force And Pressure Integral Have Distinct Registered Forms",
tags::barotrope &tags::pressure &tags::pressure_gradient &tags::quadrature
&tags::unit
) {
using EnthalpyField = mean_field::field::Field<mean_field::field::Enthalpy>;
/*
* For the registered H1 order p = 3 and n = 3:
*
* h has degree p,
* P(h) has degree 4p,
*
* so the nonlinear EOS contributes an additional
*
* 4p - p = 3p = 9
*
* beyond the registered enthalpy operand.
*/
constexpr int enthalpyOrder =
mean_field::field::Enthalpy::Scalar::familyOrder;
constexpr int pressureExtraOrder = 3 * enthalpyOrder;
constexpr int geometryWeightOrder = 2;
constexpr mean_field::quadrature::Query pressureIntegralQuery =
EnthalpyField::make_query<
mean_field::field::Enthalpy::Form::PressureIntegral>(
mean_field::quadrature::QuadratureRole::diagnostic,
geometryWeightOrder, std::array<int, 1>{pressureExtraOrder},
mean_field::utils::DOMAINS::STELLAR,
mean_field::quadrature::MappingKind::general
);
constexpr mean_field::quadrature::Query pressureForceQuery =
EnthalpyField::make_query<
mean_field::field::Enthalpy::Form::PressureForce>(
mean_field::quadrature::QuadratureRole::discretization,
geometryWeightOrder, std::array<int, 1>{pressureExtraOrder},
mean_field::utils::DOMAINS::STELLAR,
mean_field::quadrature::MappingKind::general
);
STATIC_CHECK(
mean_field::field::Enthalpy::Form::PressureIntegral::
dynamicOrderCount == 1
);
STATIC_CHECK(
mean_field::field::Enthalpy::Form::PressureForce::dynamicOrderCount == 1
);
STATIC_CHECK(
mean_field::field::Enthalpy::Form::PressureIntegral::policyKey !=
mean_field::field::Enthalpy::Form::PressureForce::policyKey
);
REQUIRE(pressureIntegralQuery.base_order.has_value());
REQUIRE(pressureForceQuery.base_order.has_value());
/*
* Pressure integral:
*
* degree(P) + degree(J)
* = 12 + 2
* = 14.
*/
CHECK(*pressureIntegralQuery.base_order == 14);
/*
* Pressure force:
*
* degree(P)
* + degree(grad w)
* + degree(J)
*
* = 12 + 2 + 2
* = 16.
*/
CHECK(*pressureForceQuery.base_order == 16);
CHECK(
pressureIntegralQuery.term ==
mean_field::quadrature::Term::pressure_integral
);
CHECK(
pressureForceQuery.term == mean_field::quadrature::Term::pressure_force
);
CHECK(
pressureIntegralQuery.role ==
mean_field::quadrature::QuadratureRole::diagnostic
);
CHECK(
pressureForceQuery.role ==
mean_field::quadrature::QuadratureRole::discretization
);
CHECK(pressureIntegralQuery.domain == mean_field::utils::DOMAINS::STELLAR);
CHECK(pressureForceQuery.domain == mean_field::utils::DOMAINS::STELLAR);
/*
* Verify that the two terms route to independent policy
* controls.
*/
mean_field::quadrature::RuleSet ruleSet =
mean_field::quadrature::make_rule_set(
mean_field::quadrature::Mode::production
);
ruleSet.pressure_integral.boost = 3;
ruleSet.pressure_force.boost = 5;
const mean_field::quadrature::Policy policy(std::move(ruleSet));
const mean_field::quadrature::Resolution pressureIntegralResolution =
policy.resolve(pressureIntegralQuery);
const mean_field::quadrature::Resolution pressureForceResolution =
policy.resolve(pressureForceQuery);
CHECK(pressureIntegralResolution.base_order == 14);
CHECK(pressureIntegralResolution.boost == 3);
CHECK(pressureIntegralResolution.order == 17);
CHECK(pressureForceResolution.base_order == 16);
CHECK(pressureForceResolution.boost == 5);
CHECK(pressureForceResolution.order == 21);
}
TEST_CASE(
"Stage Four Pressure Quadrature Exactly Integrates An N Three Polynomial",
tags::barotrope &tags::pressure &tags::pressure_gradient &tags::quadrature
&tags::accuracy
) {
using EnthalpyField = mean_field::field::Field<mean_field::field::Enthalpy>;
constexpr int enthalpyOrder =
mean_field::field::Enthalpy::Scalar::familyOrder;
constexpr int pressureExtraOrder = 3 * enthalpyOrder;
/*
* K = 1/4 and n = 3 give
*
* (n + 1) K = 1,
* rho(h) = h^3,
* P(h) = h^4 / 4.
*/
const mean_field::physics::PolytropicBarotrope barotrope(3.0, 0.25);
constexpr mean_field::quadrature::Query pressureIntegralQuery =
EnthalpyField::make_query<
mean_field::field::Enthalpy::Form::PressureIntegral>(
mean_field::quadrature::QuadratureRole::diagnostic, 0,
std::array<int, 1>{pressureExtraOrder},
mean_field::utils::DOMAINS::STELLAR,
mean_field::quadrature::MappingKind::affine
);
constexpr mean_field::quadrature::Query pressureForceQuery =
EnthalpyField::make_query<
mean_field::field::Enthalpy::Form::PressureForce>(
mean_field::quadrature::QuadratureRole::discretization, 0,
std::array<int, 1>{pressureExtraOrder},
mean_field::utils::DOMAINS::STELLAR,
mean_field::quadrature::MappingKind::affine
);
const mean_field::quadrature::RuleFactory ruleFactory{
mean_field::quadrature::Policy(
mean_field::quadrature::make_rule_set(
mean_field::quadrature::Mode::production
)
)
};
const mean_field::quadrature::MfemRule pressureIntegralRule =
ruleFactory.get(pressureIntegralQuery, mfem::Geometry::CUBE);
const mean_field::quadrature::MfemRule pressureForceRule =
ruleFactory.get(pressureForceQuery, mfem::Geometry::CUBE);
/*
* On the reference cube [0,1]^3 choose
*
* h = x^3 y^3 z^3.
*
* This is representable by the order-three H1 space.
* Then
*
* P = x^12 y^12 z^12 / 4.
*/
const double numericalPressureIntegral =
polytropic_barotrope_test_utils::integrate_cube(
*pressureIntegralRule.integration_rule,
[&barotrope](const mfem::IntegrationPoint &integrationPoint) {
const double coordinateProduct = integrationPoint.x *
integrationPoint.y *
integrationPoint.z;
const double enthalpy = std::pow(coordinateProduct, 3.0);
return barotrope.pressure_from_enthalpy(enthalpy);
}
);
const double analyticPressureIntegral = 0.25 / std::pow(13.0, 3.0);
/*
* Choose a representable vector test function whose
* divergence is
*
* div(w) = x^2 y^2 z^2.
*
* Therefore
*
* -P div(w)
* = -x^14 y^14 z^14 / 4.
*/
const double numericalPressureForceIntegral =
polytropic_barotrope_test_utils::integrate_cube(
*pressureForceRule.integration_rule,
[&barotrope](const mfem::IntegrationPoint &integrationPoint) {
const double coordinateProduct = integrationPoint.x *
integrationPoint.y *
integrationPoint.z;
const double enthalpy = std::pow(coordinateProduct, 3.0);
const double pressure =
barotrope.pressure_from_enthalpy(enthalpy);
const double testDivergence =
integrationPoint.x * integrationPoint.x *
integrationPoint.y * integrationPoint.y *
integrationPoint.z * integrationPoint.z;
return -pressure * testDivergence;
}
);
const double analyticPressureForceIntegral = -0.25 / std::pow(15.0, 3.0);
INFO(
"Pressure-integral quadrature order = "
<< pressureIntegralRule.resolution.order
);
INFO(
"Pressure-force quadrature order = "
<< pressureForceRule.resolution.order
);
INFO("Numerical pressure integral = " << numericalPressureIntegral);
INFO("Analytic pressure integral = " << analyticPressureIntegral);
INFO(
"Numerical pressure-force integral = " << numericalPressureForceIntegral
);
INFO(
"Analytic pressure-force integral = " << analyticPressureForceIntegral
);
CHECK(pressureIntegralRule.resolution.base_order == 12);
CHECK(pressureIntegralRule.resolution.order == 12);
CHECK(pressureForceRule.resolution.base_order == 14);
CHECK(pressureForceRule.resolution.order == 14);
CHECK_THAT(
numericalPressureIntegral,
Catch::Matchers::WithinAbs(analyticPressureIntegral, 5.0e-14)
);
CHECK_THAT(
numericalPressureForceIntegral,
Catch::Matchers::WithinAbs(analyticPressureForceIntegral, 5.0e-14)
);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,11 @@
#include <array>
#include <cmath>
#include <concepts>
#include <memory>
#include <stdexcept>
#include <string_view>
#include <type_traits>
#include <utility>
#include <mfem.hpp>
@@ -15,51 +18,104 @@ import test_helpers;
using namespace mean_field;
namespace {
template <typename T>
concept HasRuntimeSpaceOrders = requires(T value) { value.space_orders; };
static_assert(mean_field::field::FieldTag<field::Gravity>);
static_assert(mean_field::field::FieldTag<field::Displacement>);
static_assert(mean_field::field::FieldTag<field::Density>);
static_assert(mean_field::field::FieldTag<field::Enthalpy>);
static_assert(mean_field::field::FieldTag<field::BarotropicConstant>);
static_assert(field::Gravity::constraintsAreValid);
static_assert(std::same_as<
field::RelationTargetT<field::Gravity::Flux>,
field::Gravity::Potential>);
static_assert(!HasRuntimeSpaceOrders<utils::Args>);
std::string_view get_term_name(const quadrature::Term term) {
switch (term) {
case quadrature::Term::gravity_hdiv_mass: return "gravity_hdiv_mass";
case quadrature::Term::gravity_divergence: return "gravity_divergence";
case quadrature::Term::gravity_source: return "gravity_source";
case quadrature::Term::gravity_boundary: return "gravity_boundary";
case quadrature::Term::density_projection: return "density_projection";
case quadrature::Term::mass_conservation: return "mass_conservation";
case quadrature::Term::center_of_mass: return "center_of_mass";
case quadrature::Term::quadrupole: return "quadrupole";
case quadrature::Term::gravitational_energy: return "gravitational_energy";
case quadrature::Term::virial: return "virial";
case quadrature::Term::error_norm: return "error_norm";
case quadrature::Term::gravity_hdiv_mass:
return "gravity_hdiv_mass";
case quadrature::Term::gravity_divergence:
return "gravity_divergence";
case quadrature::Term::gravity_source:
return "gravity_source";
case quadrature::Term::gravity_boundary:
return "gravity_boundary";
case quadrature::Term::centrifugal:
return "centrifugal";
case quadrature::Term::density_projection:
return "density_projection";
case quadrature::Term::eos_closure:
return "eos_closure";
case quadrature::Term::hydrostatic_equilibrium:
return "hydrostatic_equilibrium";
case quadrature::Term::isobaric_surface:
return "isobaric_surface";
case quadrature::Term::mesh_extension:
return "mesh_extension";
case quadrature::Term::mass_conservation:
return "mass_conservation";
case quadrature::Term::mass_normalization:
return "mass_normalization";
case quadrature::Term::center_of_mass:
return "center_of_mass";
case quadrature::Term::quadrupole:
return "quadrupole";
case quadrature::Term::gravitational_energy:
return "gravitational_energy";
case quadrature::Term::pressure_integral:
return "pressure_integral";
case quadrature::Term::virial:
return "virial";
case quadrature::Term::error_norm:
return "error_norm";
case quadrature::Term::pressure_force:
return "pressure_force";
}
return "unknown";
}
quadrature::Query make_query(const quadrature::Term term, const int base_order) {
quadrature::Query make_query(
const quadrature::Term term,
const int base_order
) {
return {.term = term, .base_order = base_order};
}
}
TEST_CASE("Quadrature Policy Computes Base Orders", tags::unit & tags::quadrature) {
const quadrature::Policy policy(quadrature::make_rule_set(quadrature::Mode::production));
template <typename FieldT, typename QuantityT>
concept CanMakeFec = requires { FieldT::template make_fec<QuantityT>(3); };
} // namespace
TEST_CASE(
"Quadrature Policy Computes Base Orders",
tags::unit &tags::quadrature
) {
const quadrature::Policy policy(
quadrature::make_rule_set(quadrature::Mode::production)
);
quadrature::Query generic_query = {
.term = quadrature::Term::gravitational_energy,
.trial_order = 3,
.test_order = 4,
.coefficient_order = 2,
.term = quadrature::Term::gravitational_energy,
.trial_order = 3,
.test_order = 4,
.coefficient_order = 2,
.geometry_weight_order = 5
};
const quadrature::Resolution generic_resolution = policy.resolve(generic_query);
const quadrature::Resolution generic_resolution =
policy.resolve(generic_query);
CHECK(generic_resolution.base_order == 14);
CHECK(generic_resolution.boost == 0);
CHECK(generic_resolution.order == 14);
CHECK_FALSE(generic_resolution.used_fixed_order);
quadrature::Query divergence_query = {
.term = quadrature::Term::gravity_divergence,
.trial_order = 3,
.test_order = 2,
.coefficient_order = 1,
.term = quadrature::Term::gravity_divergence,
.trial_order = 3,
.test_order = 2,
.coefficient_order = 1,
.geometry_weight_order = 4
};
@@ -69,147 +125,218 @@ TEST_CASE("Quadrature Policy Computes Base Orders", tags::unit & tags::quadratur
CHECK(policy.resolve(divergence_query).base_order == 7);
quadrature::Query explicit_query = {
.term = quadrature::Term::gravity_hdiv_mass,
.trial_order = 20,
.test_order = 20,
.coefficient_order = 20,
.term = quadrature::Term::gravity_hdiv_mass,
.trial_order = 20,
.test_order = 20,
.coefficient_order = 20,
.geometry_weight_order = 20,
.base_order = 11
.base_order = 11
};
CHECK(policy.resolve(explicit_query).base_order == 11);
CHECK(policy.resolve(explicit_query).order == 11);
}
TEST_CASE("Quadrature Policy Composes Global and Term Boosts", tags::unit & tags::quadrature) {
quadrature::RuleSet rule_set = quadrature::make_rule_set(quadrature::Mode::production, 3);
TEST_CASE(
"Quadrature Policy Composes Global and Term Boosts",
tags::unit &tags::quadrature
) {
quadrature::RuleSet rule_set =
quadrature::make_rule_set(quadrature::Mode::production, 3);
rule_set.gravity_hdiv_mass.boost = 5;
rule_set.error_norm.boost = 2;
rule_set.error_norm.boost = 2;
const quadrature::Policy policy(rule_set);
const quadrature::Resolution mass_resolution = policy.resolve(make_query(quadrature::Term::gravity_hdiv_mass, 7));
const quadrature::Resolution mass_resolution =
policy.resolve(make_query(quadrature::Term::gravity_hdiv_mass, 7));
CHECK(mass_resolution.base_order == 7);
CHECK(mass_resolution.boost == 8);
CHECK(mass_resolution.order == 15);
CHECK_FALSE(mass_resolution.used_fixed_order);
const quadrature::Resolution error_resolution = policy.resolve(make_query(quadrature::Term::error_norm, 7));
const quadrature::Resolution error_resolution =
policy.resolve(make_query(quadrature::Term::error_norm, 7));
CHECK(error_resolution.boost == 5);
CHECK(error_resolution.order == 12);
const quadrature::Resolution source_resolution = policy.resolve(make_query(quadrature::Term::gravity_source, 7));
const quadrature::Resolution source_resolution =
policy.resolve(make_query(quadrature::Term::gravity_source, 7));
CHECK(source_resolution.boost == 3);
CHECK(source_resolution.order == 10);
}
TEST_CASE("Quadrature Fixed Orders Have Defined Precedence", tags::unit & tags::quadrature) {
quadrature::RuleSet rule_set = quadrature::make_rule_set(quadrature::Mode::production, 4);
rule_set.fallback.fixed_order = 17;
TEST_CASE(
"Quadrature Fixed Orders Have Defined Precedence",
tags::unit &tags::quadrature
) {
quadrature::RuleSet rule_set =
quadrature::make_rule_set(quadrature::Mode::production, 4);
rule_set.fallback.fixed_order = 17;
rule_set.gravity_hdiv_mass.fixed_order = 23;
rule_set.gravity_hdiv_mass.boost = 100;
rule_set.gravity_source.boost = 100;
rule_set.gravity_hdiv_mass.boost = 100;
rule_set.gravity_source.boost = 100;
const quadrature::Policy policy(rule_set);
const quadrature::Resolution term_resolution = policy.resolve(make_query(quadrature::Term::gravity_hdiv_mass, 8));
const quadrature::Resolution term_resolution =
policy.resolve(make_query(quadrature::Term::gravity_hdiv_mass, 8));
CHECK(term_resolution.base_order == 8);
CHECK(term_resolution.boost == 0);
CHECK(term_resolution.order == 23);
CHECK(term_resolution.used_fixed_order);
const quadrature::Resolution fallback_resolution = policy.resolve(make_query(quadrature::Term::gravity_source, 8));
const quadrature::Resolution fallback_resolution =
policy.resolve(make_query(quadrature::Term::gravity_source, 8));
CHECK(fallback_resolution.base_order == 8);
CHECK(fallback_resolution.boost == 0);
CHECK(fallback_resolution.order == 17);
CHECK(fallback_resolution.used_fixed_order);
}
TEST_CASE("Quadrature Modes Apply Their Expected Baseline Boosts", tags::unit & tags::quadrature) {
constexpr int base_order = 6;
TEST_CASE(
"Quadrature Modes Apply Their Expected Baseline Boosts",
tags::unit &tags::quadrature
) {
constexpr int base_order = 6;
constexpr int global_boost = 3;
for (const quadrature::Mode mode : {quadrature::Mode::fast, quadrature::Mode::production, quadrature::Mode::convergence}) {
const quadrature::Policy policy(quadrature::make_rule_set(mode, global_boost));
const quadrature::Resolution resolution = policy.resolve(make_query(quadrature::Term::error_norm, base_order));
for (const quadrature::Mode mode :
{quadrature::Mode::fast, quadrature::Mode::production,
quadrature::Mode::convergence}) {
const quadrature::Policy policy(
quadrature::make_rule_set(mode, global_boost)
);
const quadrature::Resolution resolution = policy.resolve(
make_query(quadrature::Term::error_norm, base_order)
);
CHECK(resolution.boost == global_boost);
CHECK(resolution.order == base_order + global_boost);
}
const quadrature::Policy reference_policy(quadrature::make_rule_set(quadrature::Mode::reference, global_boost));
const quadrature::Resolution reference_resolution = reference_policy.resolve(make_query(quadrature::Term::error_norm, base_order));
const quadrature::Policy reference_policy(
quadrature::make_rule_set(quadrature::Mode::reference, global_boost)
);
const quadrature::Resolution reference_resolution =
reference_policy.resolve(
make_query(quadrature::Term::error_norm, base_order)
);
CHECK(reference_resolution.boost == global_boost + 8);
CHECK(reference_resolution.order == base_order + global_boost + 8);
}
TEST_CASE("Quadrature Policy Routes Every Term to Its Control", tags::unit & tags::quadrature) {
TEST_CASE(
"Quadrature Policy Routes Every Term to Its Control",
tags::unit &tags::quadrature
) {
quadrature::RuleSet rule_set;
rule_set.gravity_hdiv_mass.boost = 1;
rule_set.gravity_divergence.boost = 2;
rule_set.gravity_source.boost = 3;
rule_set.gravity_boundary.boost = 4;
rule_set.density_projection.boost = 5;
rule_set.mass_conservation.boost = 6;
rule_set.center_of_mass.boost = 7;
rule_set.quadrupole.boost = 8;
rule_set.gravitational_energy.boost = 9;
rule_set.virial.boost = 10;
rule_set.error_norm.boost = 11;
rule_set.gravity_hdiv_mass.boost = 1;
rule_set.gravity_divergence.boost = 2;
rule_set.gravity_source.boost = 3;
rule_set.gravity_boundary.boost = 4;
rule_set.centrifugal.boost = 18;
rule_set.density_projection.boost = 5;
rule_set.eos_closure.boost = 6;
rule_set.hydrostatic_equilibrium.boost = 7;
rule_set.isobaric_surface.boost = 8;
rule_set.mesh_extension.boost = 9;
rule_set.mass_conservation.boost = 10;
rule_set.mass_normalization.boost = 11;
rule_set.center_of_mass.boost = 12;
rule_set.quadrupole.boost = 13;
rule_set.gravitational_energy.boost = 14;
rule_set.pressure_integral.boost = 15;
rule_set.virial.boost = 16;
rule_set.error_norm.boost = 17;
const quadrature::Policy policy(rule_set);
const std::array<std::pair<quadrature::Term, int>, 11> cases = {{
{quadrature::Term::gravity_hdiv_mass, 1},
{quadrature::Term::gravity_divergence, 2},
{quadrature::Term::gravity_source, 3},
{quadrature::Term::gravity_boundary, 4},
{quadrature::Term::density_projection, 5},
{quadrature::Term::mass_conservation, 6},
{quadrature::Term::center_of_mass, 7},
{quadrature::Term::quadrupole, 8},
{quadrature::Term::gravitational_energy, 9},
{quadrature::Term::virial, 10},
{quadrature::Term::error_norm, 11}
}};
const std::array<std::pair<quadrature::Term, int>, 18> cases = {
{{quadrature::Term::gravity_hdiv_mass, 1},
{quadrature::Term::gravity_divergence, 2},
{quadrature::Term::gravity_source, 3},
{quadrature::Term::gravity_boundary, 4},
{quadrature::Term::centrifugal, 18},
{quadrature::Term::density_projection, 5},
{quadrature::Term::eos_closure, 6},
{quadrature::Term::hydrostatic_equilibrium, 7},
{quadrature::Term::isobaric_surface, 8},
{quadrature::Term::mesh_extension, 9},
{quadrature::Term::mass_conservation, 10},
{quadrature::Term::mass_normalization, 11},
{quadrature::Term::center_of_mass, 12},
{quadrature::Term::quadrupole, 13},
{quadrature::Term::gravitational_energy, 14},
{quadrature::Term::pressure_integral, 15},
{quadrature::Term::virial, 16},
{quadrature::Term::error_norm, 17}}
};
for (const auto& [term, expected_boost] : cases) {
for (const auto &[term, expected_boost] : cases) {
DYNAMIC_SECTION(get_term_name(term)) {
const quadrature::Resolution resolution = policy.resolve(make_query(term, 20));
const quadrature::Resolution resolution =
policy.resolve(make_query(term, 20));
CHECK(resolution.boost == expected_boost);
CHECK(resolution.order == 20 + expected_boost);
}
}
}
TEST_CASE("Quadrature Policy Rejects Invalid Orders", tags::unit & tags::quadrature) {
const quadrature::Policy policy(quadrature::make_rule_set(quadrature::Mode::production));
TEST_CASE(
"Quadrature Policy Rejects Invalid Orders",
tags::unit &tags::quadrature
) {
const quadrature::Policy policy(
quadrature::make_rule_set(quadrature::Mode::production)
);
quadrature::Query negative_component_query = {
.term = quadrature::Term::error_norm,
.trial_order = -1
.term = quadrature::Term::error_norm, .trial_order = -1
};
REQUIRE_THROWS_AS(policy.resolve(negative_component_query), std::invalid_argument);
REQUIRE_THROWS_AS(
policy.resolve(negative_component_query), std::invalid_argument
);
quadrature::Query negative_base_query = {
.term = quadrature::Term::error_norm,
.base_order = -1
.term = quadrature::Term::error_norm, .base_order = -1
};
REQUIRE_THROWS_AS(policy.resolve(negative_base_query), std::invalid_argument);
REQUIRE_THROWS_AS(
policy.resolve(negative_base_query), std::invalid_argument
);
quadrature::RuleSet negative_fixed_rule_set;
negative_fixed_rule_set.error_norm.fixed_order = -1;
const quadrature::Policy negative_fixed_policy(negative_fixed_rule_set);
REQUIRE_THROWS_AS(negative_fixed_policy.resolve(make_query(quadrature::Term::error_norm, 3)), std::invalid_argument);
REQUIRE_THROWS_AS(
negative_fixed_policy.resolve(
make_query(quadrature::Term::error_norm, 3)
),
std::invalid_argument
);
quadrature::RuleSet negative_resolved_rule_set;
negative_resolved_rule_set.fallback.boost = -4;
const quadrature::Policy negative_resolved_policy(negative_resolved_rule_set);
REQUIRE_THROWS_AS(negative_resolved_policy.resolve(make_query(quadrature::Term::error_norm, 3)), std::invalid_argument);
const quadrature::Policy negative_resolved_policy(
negative_resolved_rule_set
);
REQUIRE_THROWS_AS(
negative_resolved_policy.resolve(
make_query(quadrature::Term::error_norm, 3)
),
std::invalid_argument
);
}
TEST_CASE("MFEM Rule Factory Returns the Resolved Rule", tags::unit & tags::quadrature) {
quadrature::RuleSet rule_set = quadrature::make_rule_set(quadrature::Mode::production, 2);
TEST_CASE(
"MFEM Rule Factory Returns the Resolved Rule",
tags::unit &tags::quadrature
) {
quadrature::RuleSet rule_set =
quadrature::make_rule_set(quadrature::Mode::production, 2);
rule_set.error_norm.boost = 3;
const quadrature::RuleFactory factory{quadrature::Policy(rule_set)};
const quadrature::MfemRule selected_rule = factory.get(make_query(quadrature::Term::error_norm, 4), mfem::Geometry::CUBE);
const mfem::IntegrationRule& expected_rule = mfem::IntRules.Get(mfem::Geometry::CUBE, 9);
const quadrature::MfemRule selected_rule = factory.get(
make_query(quadrature::Term::error_norm, 4), mfem::Geometry::CUBE
);
const mfem::IntegrationRule &expected_rule =
mfem::IntRules.Get(mfem::Geometry::CUBE, 9);
REQUIRE(selected_rule.integration_rule != nullptr);
CHECK(selected_rule.resolution.base_order == 4);
@@ -219,47 +346,85 @@ TEST_CASE("MFEM Rule Factory Returns the Resolved Rule", tags::unit & tags::quad
CHECK(selected_rule.integration_rule->GetNPoints() > 0);
}
TEST_CASE("MFEM Quadrature Rule Integrates Tensor Polynomial Exactly", tags::unit & tags::quadrature) {
TEST_CASE(
"MFEM Quadrature Rule Integrates Tensor Polynomial Exactly",
tags::unit &tags::quadrature
) {
constexpr int polynomial_degree = 7;
const quadrature::RuleFactory factory{quadrature::Policy(quadrature::make_rule_set(quadrature::Mode::production))};
const quadrature::MfemRule selected_rule = factory.get(make_query(quadrature::Term::error_norm, polynomial_degree), mfem::Geometry::CUBE);
const quadrature::RuleFactory factory{quadrature::Policy(
quadrature::make_rule_set(quadrature::Mode::production)
)};
const quadrature::MfemRule selected_rule = factory.get(
make_query(quadrature::Term::error_norm, polynomial_degree),
mfem::Geometry::CUBE
);
double numerical_integral = 0.0;
for (int i = 0; i < selected_rule.integration_rule->GetNPoints(); ++i) {
const mfem::IntegrationPoint& integration_point = selected_rule.integration_rule->IntPoint(i);
numerical_integral += integration_point.weight * std::pow(integration_point.x, polynomial_degree) * std::pow(integration_point.y, polynomial_degree) * std::pow(integration_point.z, polynomial_degree);
const mfem::IntegrationPoint &integration_point =
selected_rule.integration_rule->IntPoint(i);
numerical_integral += integration_point.weight *
std::pow(integration_point.x, polynomial_degree) *
std::pow(integration_point.y, polynomial_degree) *
std::pow(integration_point.z, polynomial_degree);
}
const double one_dimensional_integral = 1.0 / static_cast<double>(polynomial_degree + 1);
const double analytic_integral = one_dimensional_integral * one_dimensional_integral * one_dimensional_integral;
CHECK_THAT(numerical_integral, Catch::Matchers::WithinAbs(analytic_integral, 5.0e-14));
const double one_dimensional_integral =
1.0 / static_cast<double>(polynomial_degree + 1);
const double analytic_integral = one_dimensional_integral *
one_dimensional_integral *
one_dimensional_integral;
CHECK_THAT(
numerical_integral,
Catch::Matchers::WithinAbs(analytic_integral, 5.0e-14)
);
}
TEST_CASE("Policy Controlled Hdiv Mass Assembly Matches Overintegrated Reference", tags::quadrature & tags::solver & tags::integration) {
mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D(1, 1, 1, mfem::Element::HEXAHEDRON, 1.0, 1.0, 1.0);
TEST_CASE(
"Policy Controlled Hdiv Mass Assembly Matches Overintegrated Reference",
tags::quadrature &tags::solver &tags::integration
) {
mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D(
1, 1, 1, mfem::Element::HEXAHEDRON, 1.0, 1.0, 1.0
);
mfem::RT_FECollection rt_collection(2, 3);
mfem::FiniteElementSpace rt_space(&mesh, &rt_collection);
const mfem::FiniteElement* rt_element = rt_space.GetTypicalFE();
mfem::ElementTransformation* transformation = mesh.GetElementTransformation(0);
const int base_order = 2 * rt_element->GetOrder() + transformation->OrderW();
const mfem::FiniteElement *rt_element = rt_space.GetTypicalFE();
mfem::ElementTransformation *transformation =
mesh.GetElementTransformation(0);
const int base_order =
2 * rt_element->GetOrder() + transformation->OrderW();
const quadrature::RuleFactory production_factory{quadrature::Policy(quadrature::make_rule_set(quadrature::Mode::production))};
const quadrature::MfemRule production_rule = production_factory.get(make_query(quadrature::Term::gravity_hdiv_mass, base_order), rt_element->GetGeomType());
const quadrature::RuleFactory production_factory{quadrature::Policy(
quadrature::make_rule_set(quadrature::Mode::production)
)};
const quadrature::MfemRule production_rule = production_factory.get(
make_query(quadrature::Term::gravity_hdiv_mass, base_order),
rt_element->GetGeomType()
);
quadrature::RuleSet reference_rule_set = quadrature::make_rule_set(quadrature::Mode::production);
quadrature::RuleSet reference_rule_set =
quadrature::make_rule_set(quadrature::Mode::production);
reference_rule_set.gravity_hdiv_mass.boost = 8;
const quadrature::RuleFactory reference_factory{quadrature::Policy(reference_rule_set)};
const quadrature::MfemRule reference_rule = reference_factory.get(make_query(quadrature::Term::gravity_hdiv_mass, base_order), rt_element->GetGeomType());
const quadrature::RuleFactory reference_factory{
quadrature::Policy(reference_rule_set)
};
const quadrature::MfemRule reference_rule = reference_factory.get(
make_query(quadrature::Term::gravity_hdiv_mass, base_order),
rt_element->GetGeomType()
);
mfem::BilinearForm production_mass(&rt_space);
auto* production_integrator = new mfem::VectorFEMassIntegrator();
production_integrator->SetIntegrationRule(*production_rule.integration_rule);
auto *production_integrator = new mfem::VectorFEMassIntegrator();
production_integrator->SetIntegrationRule(
*production_rule.integration_rule
);
production_mass.AddDomainIntegrator(production_integrator);
production_mass.Assemble();
production_mass.Finalize();
mfem::BilinearForm reference_mass(&rt_space);
auto* reference_integrator = new mfem::VectorFEMassIntegrator();
auto *reference_integrator = new mfem::VectorFEMassIntegrator();
reference_integrator->SetIntegrationRule(*reference_rule.integration_rule);
reference_mass.AddDomainIntegrator(reference_integrator);
reference_mass.Assemble();
@@ -277,55 +442,417 @@ TEST_CASE("Policy Controlled Hdiv Mass Assembly Matches Overintegrated Reference
mfem::Vector difference(production_output);
difference -= reference_output;
const double relative_difference = difference.Norml2() / reference_output.Norml2();
const double relative_difference =
difference.Norml2() / reference_output.Norml2();
INFO("Production quadrature order = " << production_rule.resolution.order);
INFO("Reference quadrature order = " << reference_rule.resolution.order);
INFO("Relative operator difference = " << relative_difference);
CHECK_THAT(relative_difference, Catch::Matchers::WithinAbs(0.0, 1.0e-12));
}
TEST_CASE("HDiv Mass Helper Resolves the MFEM Baseline", tags::unit & tags::quadrature & tags::solver) {
mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D(1, 1, 1, mfem::Element::HEXAHEDRON);
TEST_CASE(
"HDiv Mass Helper Resolves the MFEM Baseline",
tags::unit &tags::quadrature &tags::solver
) {
mfem::Mesh mesh =
mfem::Mesh::MakeCartesian3D(1, 1, 1, mfem::Element::HEXAHEDRON);
mfem::RT_FECollection rt_collection(2, 3);
mfem::FiniteElementSpace rt_space(&mesh, &rt_collection);
quadrature::RuleSet rule_set = quadrature::make_rule_set(quadrature::Mode::production);
quadrature::RuleSet rule_set =
quadrature::make_rule_set(quadrature::Mode::production);
rule_set.gravity_hdiv_mass.boost = 3;
quadrature::RuleFactory factory{quadrature::Policy(std::move(rule_set))};
const mfem::FiniteElement& element = *rt_space.GetTypicalFE();
const mfem::ElementTransformation& transformation = *mesh.GetElementTransformation(0);
const mfem::FiniteElement &element = *rt_space.GetTypicalFE();
const mfem::ElementTransformation &transformation =
*mesh.GetElementTransformation(0);
mfem::VectorFEMassIntegrator integrator;
const quadrature::Resolution resolution = factory.configure_gravity_hdiv_mass(integrator, quadrature::QuadratureRole::discretization, element, transformation);
const int expected_base_order = 2 * element.GetOrder() + transformation.OrderW();
const quadrature::Resolution resolution =
factory.configure_gravity_hdiv_mass(
integrator, quadrature::QuadratureRole::discretization, element,
transformation
);
const int expected_base_order =
2 * element.GetOrder() + transformation.OrderW();
CHECK(resolution.base_order == expected_base_order);
CHECK(resolution.boost == 3);
CHECK(resolution.order == expected_base_order + 3);
}
TEST_CASE("Gravity Divergence Helper Resolves Preconditioner Rule", tags::unit & tags::quadrature & tags::solver) {
mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D(1, 1, 1, mfem::Element::HEXAHEDRON);
TEST_CASE(
"Gravity Divergence Helper Resolves Preconditioner Rule",
tags::unit &tags::quadrature &tags::solver
) {
mfem::Mesh mesh =
mfem::Mesh::MakeCartesian3D(1, 1, 1, mfem::Element::HEXAHEDRON);
mfem::RT_FECollection rt_collection(2, 3);
mfem::L2_FECollection l2_collection(2, 3);
mfem::FiniteElementSpace rt_space(&mesh, &rt_collection);
mfem::FiniteElementSpace l2_space(&mesh, &l2_collection);
quadrature::RuleSet rule_set = quadrature::make_rule_set(quadrature::Mode::production);
rule_set.gravity_divergence.boost = 2;
quadrature::RuleSet rule_set =
quadrature::make_rule_set(quadrature::Mode::production);
rule_set.gravity_divergence.boost = 2;
rule_set.roles.preconditioner.boost = 3;
quadrature::RuleFactory factory{quadrature::Policy(std::move(rule_set))};
const mfem::FiniteElement& trial_element = *rt_space.GetTypicalFE();
const mfem::FiniteElement& test_element = *l2_space.GetTypicalFE();
const mfem::ElementTransformation& transformation = *mesh.GetElementTransformation(0);
const mfem::FiniteElement &trial_element = *rt_space.GetTypicalFE();
const mfem::FiniteElement &test_element = *l2_space.GetTypicalFE();
const mfem::ElementTransformation &transformation =
*mesh.GetElementTransformation(0);
mfem::VectorFEDivergenceIntegrator integrator;
const quadrature::Resolution resolution = factory.configure_gravity_divergence(integrator, quadrature::QuadratureRole::preconditioner, trial_element, test_element, transformation);
const int expected_base_order = std::max(0, trial_element.GetOrder() - 1) + test_element.GetOrder() + transformation.OrderW();
const quadrature::Resolution resolution =
factory.configure_gravity_divergence(
integrator, quadrature::QuadratureRole::preconditioner,
trial_element, test_element, transformation
);
const int expected_base_order = std::max(0, trial_element.GetOrder() - 1) +
test_element.GetOrder() +
transformation.OrderW();
CHECK(resolution.base_order == expected_base_order);
CHECK(resolution.boost == 5);
CHECK(resolution.order == expected_base_order + 5);
}
TEST_CASE(
"Field Registry Encodes Spaces Orders And Stable Pair Constraints",
tags::unit &tags::quadrature &tags::initialization
) {
CHECK(mean_field::field::Density::Scalar::familyOrder == 2);
CHECK(mean_field::field::Gravity::Potential::familyOrder == 2);
CHECK(mean_field::field::Gravity::Flux::familyOrder == 2);
CHECK(mean_field::field::Displacement::Vector::familyOrder == 3);
CHECK(mean_field::field::Enthalpy::Scalar::familyOrder == 3);
CHECK((std::same_as<
mean_field::field::Density::Scalar::Space, mean_field::field::L2>));
CHECK((
std::same_as<
mean_field::field::Gravity::Potential::Space, mean_field::field::L2>
));
CHECK((std::same_as<
mean_field::field::Gravity::Flux::Space, mean_field::field::RT>));
CHECK((std::same_as<
mean_field::field::Displacement::Vector::Space,
mean_field::field::H1>));
CHECK((std::same_as<
mean_field::field::Enthalpy::Scalar::Space, mean_field::field::H1>));
CHECK(mean_field::field::Density::Scalar::rankValue == 0);
CHECK(mean_field::field::Gravity::Potential::rankValue == 0);
CHECK(mean_field::field::Gravity::Flux::rankValue == 1);
CHECK(mean_field::field::Displacement::Vector::rankValue == 1);
CHECK(mean_field::field::Enthalpy::Scalar::rankValue == 0);
CHECK(
mean_field::field::Gravity::Flux::familyOrder ==
mean_field::field::Gravity::Potential::familyOrder
);
CHECK(
std::is_empty_v<mean_field::field::Field<mean_field::field::Gravity>>
);
CHECK(
std::is_empty_v<
mean_field::field::Field<mean_field::field::Displacement>>
);
CHECK(
std::is_empty_v<mean_field::field::Field<mean_field::field::Density>>
);
CHECK(
std::is_empty_v<mean_field::field::Field<mean_field::field::Enthalpy>>
);
STATIC_CHECK(field::RegisteredQuantity<field::BarotropicConstant::Scalar>);
STATIC_CHECK(
field::GlobalScalarQuantity<field::BarotropicConstant::Scalar>
);
STATIC_CHECK_FALSE(field::FieldQuantity<field::BarotropicConstant::Scalar>);
STATIC_CHECK(
field::BarotropicConstant::Scalar::storageKind ==
field::StorageKind::global_scalar
);
STATIC_CHECK(field::BarotropicConstant::Scalar::staticBlockSize == 1);
STATIC_CHECK(
field::Enthalpy::Scalar::storageKind ==
field::StorageKind::finite_element
);
STATIC_CHECK(
field::Enthalpy::Scalar::staticBlockSize == field::dynamicBlockSize
);
STATIC_CHECK_FALSE(
CanMakeFec<
field::Field<field::BarotropicConstant>,
field::BarotropicConstant::Scalar>
);
}
TEST_CASE(
"Field Forms Produce Typed Quadrature Queries",
tags::unit &tags::quadrature
) {
using GravityField = field::Field<field::Gravity>;
using DensityField = field::Field<field::Density>;
using EnthalpyField = field::Field<field::Enthalpy>;
constexpr quadrature::Query hdiv_query =
GravityField::make_query<field::Gravity::Form::HDivMass>(
quadrature::QuadratureRole::discretization, 2, {},
utils::DOMAINS::ALL, quadrature::MappingKind::general
);
constexpr quadrature::Query divergence_query =
GravityField::make_query<field::Gravity::Form::DivergenceCoupling>(
quadrature::QuadratureRole::preconditioner, 2
);
constexpr quadrature::Query source_query =
GravityField::make_query<field::Gravity::Form::SourceProjection>(
quadrature::QuadratureRole::projection, 2, {},
utils::DOMAINS::STELLAR
);
constexpr quadrature::Query center_of_mass_query =
DensityField::make_query<field::Density::Form::CenterOfMass>(
quadrature::QuadratureRole::diagnostic, 2, std::array<int, 1>{1},
utils::DOMAINS::STELLAR
);
constexpr quadrature::Query eos_closure_query =
EnthalpyField::make_query<field::Enthalpy::Form::EosClosureSource>(
quadrature::QuadratureRole::discretization, 2,
std::array<int, 1>{6}, utils::DOMAINS::STELLAR,
quadrature::MappingKind::general
);
constexpr quadrature::Query equilibrium_gravity_query =
EnthalpyField::make_query<field::Enthalpy::Form::EquilibriumGravity>(
quadrature::QuadratureRole::discretization, 2, {},
utils::DOMAINS::STELLAR, quadrature::MappingKind::general
);
constexpr quadrature::Query equilibrium_constant_query =
EnthalpyField::make_query<field::Enthalpy::Form::EquilibriumConstant>(
quadrature::QuadratureRole::discretization, 2, {},
utils::DOMAINS::STELLAR, quadrature::MappingKind::general
);
constexpr quadrature::Query rotation_query =
EnthalpyField::make_query<field::Enthalpy::Form::EquilibriumRotation>(
quadrature::QuadratureRole::discretization, 2,
std::array<int, 1>{2}, utils::DOMAINS::STELLAR,
quadrature::MappingKind::general
);
constexpr quadrature::Query isobaric_surface_query =
EnthalpyField::make_query<field::Enthalpy::Form::IsobaricSurface>(
quadrature::QuadratureRole::discretization, 2, {},
utils::DOMAINS::STELLAR, quadrature::MappingKind::general
);
constexpr quadrature::Query mesh_extension_query =
field::Field<field::Displacement>::make_query<
field::Displacement::Form::MeshExtension>(
quadrature::QuadratureRole::discretization, 2, {},
utils::DOMAINS::ALL, quadrature::MappingKind::general
);
constexpr quadrature::Query mass_normalization_query =
DensityField::make_query<field::Density::Form::MassNormalization>(
quadrature::QuadratureRole::discretization, 2, {},
utils::DOMAINS::STELLAR, quadrature::MappingKind::general
);
constexpr quadrature::Query pressure_integral_query =
EnthalpyField::make_query<field::Enthalpy::Form::PressureIntegral>(
quadrature::QuadratureRole::diagnostic, 2, std::array<int, 1>{9},
utils::DOMAINS::STELLAR, quadrature::MappingKind::general
);
STATIC_CHECK(
mean_field::field::Gravity::Form::SourceProjection::dynamicOrderCount ==
0
);
STATIC_CHECK(hdiv_query.base_order.has_value());
STATIC_CHECK(*hdiv_query.base_order == 8);
STATIC_CHECK(*divergence_query.base_order == 6);
STATIC_CHECK(*source_query.base_order == 6);
STATIC_CHECK(*center_of_mass_query.base_order == 5);
STATIC_CHECK(*eos_closure_query.base_order == 13);
STATIC_CHECK(*equilibrium_gravity_query.base_order == 7);
STATIC_CHECK(*rotation_query.base_order == 7);
STATIC_CHECK(*isobaric_surface_query.base_order == 8);
STATIC_CHECK(*mesh_extension_query.base_order == 6);
STATIC_CHECK(*mass_normalization_query.base_order == 4);
STATIC_CHECK(*pressure_integral_query.base_order == 14);
STATIC_CHECK(*equilibrium_constant_query.base_order == 5);
CHECK(
equilibrium_constant_query.term ==
quadrature::Term::hydrostatic_equilibrium
);
CHECK(hdiv_query.term == mean_field::quadrature::Term::gravity_hdiv_mass);
CHECK(
hdiv_query.role ==
mean_field::quadrature::QuadratureRole::discretization
);
CHECK(hdiv_query.domain == mean_field::utils::DOMAINS::ALL);
CHECK(hdiv_query.mapping == mean_field::quadrature::MappingKind::general);
CHECK(source_query.term == mean_field::quadrature::Term::gravity_source);
CHECK(
center_of_mass_query.term ==
mean_field::quadrature::Term::center_of_mass
);
CHECK(eos_closure_query.term == mean_field::quadrature::Term::eos_closure);
CHECK(
equilibrium_gravity_query.term ==
mean_field::quadrature::Term::hydrostatic_equilibrium
);
CHECK(
rotation_query.term ==
mean_field::quadrature::Term::hydrostatic_equilibrium
);
CHECK(
isobaric_surface_query.term ==
mean_field::quadrature::Term::isobaric_surface
);
CHECK(
mesh_extension_query.term ==
mean_field::quadrature::Term::mesh_extension
);
CHECK(
mass_normalization_query.term ==
mean_field::quadrature::Term::mass_normalization
);
CHECK(
pressure_integral_query.term ==
mean_field::quadrature::Term::pressure_integral
);
CHECK_THROWS_AS(
(GravityField::make_query<mean_field::field::Gravity::Form::HDivMass>(
mean_field::quadrature::QuadratureRole::discretization, -1
)),
std::invalid_argument
);
CHECK_THROWS_AS(
(DensityField::make_query<
mean_field::field::Density::Form::CenterOfMass>(
mean_field::quadrature::QuadratureRole::diagnostic, 2,
std::array<int, 1>{-1}
)),
std::invalid_argument
);
}
TEST_CASE(
"Field Factories Construct The Registered MFEM Collections",
tags::unit &tags::quadrature &tags::initialization
) {
using GravityField = field::Field<field::Gravity>;
using DisplacementField = field::Field<field::Displacement>;
using DensityField = field::Field<field::Density>;
using EnthalpyField = field::Field<field::Enthalpy>;
std::unique_ptr<mfem::FiniteElementCollection> density_collection =
DensityField::make_fec<field::Density::Scalar>(3);
std::unique_ptr<mfem::FiniteElementCollection> potential_collection =
GravityField::make_fec<field::Gravity::Potential>(3);
std::unique_ptr<mfem::FiniteElementCollection> flux_collection =
GravityField::make_fec<field::Gravity::Flux>(3);
std::unique_ptr<mfem::FiniteElementCollection> displacement_collection =
DisplacementField::make_fec<field::Displacement::Vector>(3);
std::unique_ptr<mfem::FiniteElementCollection> enthalpy_collection =
EnthalpyField::make_fec<field::Enthalpy::Scalar>(3);
CHECK(
dynamic_cast<mfem::L2_FECollection *>(density_collection.get()) !=
nullptr
);
CHECK(
dynamic_cast<mfem::L2_FECollection *>(potential_collection.get()) !=
nullptr
);
CHECK(
dynamic_cast<mfem::RT_FECollection *>(flux_collection.get()) != nullptr
);
CHECK(
dynamic_cast<mfem::H1_FECollection *>(displacement_collection.get()) !=
nullptr
);
CHECK(
dynamic_cast<mfem::H1_FECollection *>(enthalpy_collection.get()) !=
nullptr
);
CHECK_THROWS_AS(
(DensityField::make_fec<mean_field::field::Density::Scalar>(0)),
std::invalid_argument
);
}
TEST_CASE(
"FEM Setup Realizes Every Registered Field Independently",
tags::integration &tags::initialization &tags::solver
) {
const utils::Args args = test_utils::setup_args();
const fem::FEM fem = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(fem.okay());
REQUIRE(fem.mesh != nullptr);
REQUIRE(fem.densityFes != nullptr);
REQUIRE(fem.gravityPotentialFes != nullptr);
REQUIRE(fem.gravityFluxFes != nullptr);
REQUIRE(fem.displacementFes != nullptr);
REQUIRE(fem.enthalpyFes != nullptr);
CHECK(fem.densityFes.get() != fem.gravityPotentialFes.get());
CHECK(fem.densityFec.get() != fem.gravityPotentialFec.get());
CHECK(
fem.densityFes->GetMaxElementOrder() ==
mean_field::field::Density::Scalar::familyOrder
);
CHECK(
fem.gravityPotentialFes->GetMaxElementOrder() ==
mean_field::field::Gravity::Potential::familyOrder
);
CHECK(
fem.gravityFluxFes->GetMaxElementOrder() ==
mean_field::field::Gravity::Flux::familyOrder + 1
);
CHECK(
fem.displacementFes->GetMaxElementOrder() ==
mean_field::field::Displacement::Vector::familyOrder
);
CHECK(
fem.enthalpyFes->GetMaxElementOrder() ==
mean_field::field::Enthalpy::Scalar::familyOrder
);
CHECK(fem.densityFes->GetVDim() == 1);
CHECK(fem.gravityPotentialFes->GetVDim() == 1);
CHECK(fem.gravityFluxFes->GetVDim() == 1);
CHECK(fem.displacementFes->GetVDim() == fem.mesh->SpaceDimension());
CHECK(fem.displacementFes->GetOrdering() == mfem::Ordering::byNODES);
CHECK(fem.enthalpyFes->GetVDim() == 1);
REQUIRE(fem.blockTrueOffsets.Size() == 3);
CHECK(fem.blockTrueOffsets[0] == 0);
CHECK(fem.blockTrueOffsets[1] == fem.displacementFes->GetTrueVSize());
CHECK(
fem.blockTrueOffsets[2] ==
fem.displacementFes->GetTrueVSize() + fem.densityFes->GetTrueVSize()
);
REQUIRE(fem.gravityBlockTrueOffsets.Size() == 3);
CHECK(fem.gravityBlockTrueOffsets[0] == 0);
CHECK(fem.gravityBlockTrueOffsets[1] == fem.gravityFluxFes->GetTrueVSize());
CHECK(
fem.gravityBlockTrueOffsets[2] ==
fem.gravityFluxFes->GetTrueVSize() +
fem.gravityPotentialFes->GetTrueVSize()
);
CHECK(
fem.gravityContext.source_form->Height() ==
fem.gravityPotentialFes->GetTrueVSize()
);
}

View File

@@ -1,23 +1,31 @@
module;
#include <string>
#include <array>
#include <algorithm>
#include <array>
#include <catch2/internal/catch_stringref.hpp>
#include <string>
#include <mfem.hpp>
#include <optional>
#include <utility>
export module test_helpers;
import mean_field;
template <std::size_t N>
struct Tag {
template <std::size_t N> struct Tag {
std::array<char, N> chars{};
// ReSharper disable once CppNonExplicitConvertingConstructor
consteval Tag(std::array<char, N> arr) : chars(arr) {}
consteval Tag(
std::array<
char,
N> arr
)
: chars(arr) {
}
// ReSharper disable once CppNonExplicitConversionOperator
constexpr operator const char*() const { return chars.data(); }
constexpr operator const char *() const {
return chars.data();
}
// ReSharper disable once CppNonExplicitConversionOperator
constexpr operator Catch::StringRef() const {
@@ -25,7 +33,7 @@ struct Tag {
}
template <std::size_t M>
consteval Tag<N + M - 1> operator&(const Tag<M>& other) const {
consteval Tag<N + M - 1> operator&(const Tag<M> &other) const {
std::array<char, N + M - 1> res{};
std::ranges::copy(chars.begin(), chars.end() - 1, res.begin());
std::ranges::copy(other.chars, res.begin() + (N - 1));
@@ -33,18 +41,22 @@ struct Tag {
}
};
template <std::size_t N>
consteval auto make_tag(const char (&str)[N]) {
template <std::size_t N> consteval auto make_tag(const char (&str)[N]) {
std::array<char, N + 2> res{};
res[0] = '[';
std::ranges::copy(str, str + N - 1, res.begin() + 1);
res[N] = ']';
res[N] = ']';
res[N + 1] = '\0';
return Tag<N + 2>{res};
}
template <std::size_t N, std::size_t M>
consteval auto sub_tag(const Tag<N>& parent, const char (&str)[M]) {
template <
std::size_t N,
std::size_t M>
consteval auto sub_tag(
const Tag<N> &parent,
const char (&str)[M]
) {
return parent & make_tag(str);
}
@@ -54,11 +66,11 @@ namespace test_utils::detail {
mean_field::utils::Args make_default_args() {
mean_field::utils::Args args;
args.mesh_file = "sandbox.smesh";
args.p.rtol = 1.0e-12;
args.p.atol = 1.0e-12;
args.p.rtol = 1.0e-12;
args.p.atol = 1.0e-12;
return args;
}
}
} // namespace test_utils::detail
export namespace test_utils {
void set_args(mean_field::utils::Args args) {
@@ -72,26 +84,216 @@ export namespace test_utils {
return detail::make_default_args();
}
}
} // namespace test_utils
export namespace gravity_prepared_test_utils {
inline mfem::Vector make_deterministic_vector(
const int size,
const double phase = 0.0
) {
mfem::Vector vector(size);
for (int i = 0; i < size; ++i) {
const double index = static_cast<double>(i + 1);
vector(i) = std::sin(0.37 * index + phase) +
0.31 * std::cos(0.19 * index - 0.5 * phase);
}
return vector;
}
inline mfem::Vector make_displacement(
const mean_field::fem::FEM &f,
const double scale
) {
mfem::ParGridFunction displacement(f.displacementFes.get());
auto displacement_function =
[scale](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(3);
value(0) = scale * (0.04 * position(0) +
0.01 * position(1) * position(2));
value(1) = scale * (-0.03 * position(1) +
0.008 * position(0) * position(2));
value(2) = scale * (0.02 * position(2) -
0.006 * position(0) * position(1));
};
mfem::VectorFunctionCoefficient coefficient(
f.mesh->Dimension(), displacement_function
);
displacement.ProjectCoefficient(coefficient);
mfem::Vector displacement_true;
displacement.GetTrueDofs(displacement_true);
return displacement_true;
}
inline mfem::Vector make_domain_supported_density(
const mean_field::fem::FEM &f,
const bool stellar
) {
mfem::Vector attribute_values(f.mesh->attributes.Max());
attribute_values = 0.0;
const int vacuum_attribute =
f.domainMapperStateless->GetVacuumElementAttribute();
for (int i = 0; i < f.mesh->attributes.Size(); ++i) {
const int attribute = f.mesh->attributes[i];
const bool is_stellar = attribute != vacuum_attribute;
if (is_stellar == stellar) {
attribute_values(attribute - 1) = 1.0;
}
}
mfem::PWConstCoefficient coefficient(attribute_values);
mfem::ParGridFunction density(f.densityFes.get());
density.ProjectCoefficient(coefficient);
mfem::Vector density_true;
density.GetTrueDofs(density_true);
return density_true;
}
inline mfem::Vector linear_combination(
const mfem::Vector &first,
const double first_scale,
const mfem::Vector &second,
const double second_scale
) {
MFEM_VERIFY(
first.Size() == second.Size(),
"Cannot combine vectors with different sizes."
);
mfem::Vector combination(first);
combination *= first_scale;
combination.Add(second_scale, second);
return combination;
}
inline double global_norm(
const mfem::Vector &vector,
MPI_Comm communicator
) {
const double local_norm_squared = vector * vector;
double global_norm_squared = 0.0;
MPI_Allreduce(
&local_norm_squared, &global_norm_squared, 1, MPI_DOUBLE, MPI_SUM,
communicator
);
return std::sqrt(global_norm_squared);
}
inline double global_dot(
const mfem::Vector &first,
const mfem::Vector &second,
MPI_Comm communicator
) {
MFEM_VERIFY(
first.Size() == second.Size(),
"Cannot take the dot product of vectors with different sizes."
);
const double local_dot = first * second;
double global_dot = 0.0;
MPI_Allreduce(
&local_dot, &global_dot, 1, MPI_DOUBLE, MPI_SUM, communicator
);
return global_dot;
}
inline double relative_error(
const mfem::Vector &computed,
const mfem::Vector &reference,
MPI_Comm communicator
) {
MFEM_VERIFY(
computed.Size() == reference.Size(),
"Cannot compare vectors with different sizes."
);
mfem::Vector difference(computed);
difference -= reference;
return global_norm(difference, communicator) /
std::max(
global_norm(reference, communicator),
std::numeric_limits<double>::epsilon()
);
}
inline double relative_scalar_error(
const double computed,
const double reference
) {
return std::abs(computed - reference) /
std::max(
std::abs(reference), std::numeric_limits<double>::epsilon()
);
}
} // namespace gravity_prepared_test_utils
export namespace tags {
inline constexpr auto geometry = make_tag("geometry");
inline constexpr auto physics = make_tag("physics");
inline constexpr auto unit = make_tag("unit");
inline constexpr auto mesh = make_tag("mesh");
inline constexpr auto integration = make_tag("integration");
inline constexpr auto solver = make_tag("solver");
inline constexpr auto geometry = make_tag("geometry");
inline constexpr auto physics = make_tag("physics");
inline constexpr auto unit = make_tag("unit");
inline constexpr auto mesh = make_tag("mesh");
inline constexpr auto integration = make_tag("integration");
inline constexpr auto solver = make_tag("solver");
inline constexpr auto integrator = make_tag("integrator");
inline constexpr auto mapping = make_tag("mapping");
inline constexpr auto utils = make_tag("utils");
inline constexpr auto mfem_operators = make_tag("operators");
inline constexpr auto initialization = make_tag("initialization");
inline constexpr auto accuracy = make_tag("accuracy");
inline constexpr auto closure = make_tag("closure");
inline constexpr auto kernels = make_tag("kernels");
inline constexpr auto gravity = sub_tag(physics, "gravity");
inline constexpr auto hydro = sub_tag(physics, "hydro");
inline constexpr auto jacobian = sub_tag(integration & physics , "jacobian");
inline constexpr auto residuals = sub_tag(integration & physics , "residuals");
inline constexpr auto h_refinement = sub_tag(mesh & solver , "h_refinement");
inline constexpr auto volume = sub_tag(mesh & geometry , "volume");
inline constexpr auto quadrature = sub_tag(mesh & geometry & solver , "quadrature");
inline constexpr auto legacy_comparison = make_tag("legacy_comparison");
inline constexpr auto pressure = sub_tag(physics, "pressure");
inline constexpr auto analytic_comparison = sub_tag(solver & physics & residuals , "analytic_comparison");
inline constexpr auto self_consistency = sub_tag(solver & physics , "self_consistency");
inline constexpr auto hydro = sub_tag(physics, "hydro");
inline constexpr auto jacobian = sub_tag(integration & physics, "jacobian");
inline constexpr auto residuals =
sub_tag(integration & physics, "residuals");
inline constexpr auto volume = sub_tag(mesh & geometry, "volume");
inline constexpr auto quadrature =
sub_tag(mesh & geometry & solver, "quadrature");
inline constexpr auto convergence = sub_tag(solver, "convergence");
inline constexpr auto transformations =
sub_tag(mesh & geometry, "transformations");
}
inline constexpr auto h_refinement =
sub_tag(mesh & convergence, "h_refinement");
inline constexpr auto p_refinement =
sub_tag(mesh & convergence, "p_refinement");
inline constexpr auto analytic_comparison =
sub_tag(solver & physics & residuals, "analytic_comparison");
inline constexpr auto self_consistency =
sub_tag(solver & physics, "self_consistency");
inline constexpr auto centrifugal =
sub_tag(solver & physics, "centrifugal");
inline constexpr auto advection = sub_tag(solver & physics, "advection");
inline constexpr auto coriolis = sub_tag(solver & physics, "coriolis");
inline constexpr auto gravity = sub_tag(solver & physics, "gravity");
inline constexpr auto enthalpy = sub_tag(solver & physics, "enthalpy");
inline constexpr auto barotrope = sub_tag(physics, "barotrope");
inline constexpr auto mass_continuity =
sub_tag(solver & physics, "mass_continuity");
inline constexpr auto pressure_gradient =
sub_tag(solver & physics, "pressure_gradient");
inline constexpr auto viscosity = sub_tag(solver & physics, "viscosity");
inline constexpr auto compactification =
sub_tag(mesh & mapping, "compactification");
inline constexpr auto kelvin = sub_tag(compactification, "kelvin");
inline constexpr auto prepared = sub_tag(solver & physics, "prepared");
inline constexpr auto contexts = sub_tag(solver, "contexts");
} // namespace tags

View File

@@ -1,127 +1,496 @@
#include <algorithm>
#include <catch2/catch_session.hpp>
#include <catch2/reporters/catch_reporter_streaming_base.hpp>
#include <catch2/reporters/catch_reporter_registrars.hpp>
#include <catch2/catch_test_case_info.hpp>
#include <catch2/reporters/catch_reporter_registrars.hpp>
#include <catch2/reporters/catch_reporter_streaming_base.hpp>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <mfem.hpp>
#include <string>
#include <iomanip>
#include <vector>
#include <algorithm>
#include <regex>
#include <sstream>
#include <string>
#include <string_view>
#include <vector>
#include <fourdst/config/config.h>
#include <CLI/CLI.hpp>
#include <fourdst/config/config.h>
import mean_field;
import test_helpers;
std::string escapeHtml(const std::string &data) {
std::string buffer;
buffer.reserve(data.size());
for (size_t pos = 0; pos != data.size(); ++pos) {
switch (data[pos]) {
case '&':
buffer.append("&amp;");
break;
case '\"':
buffer.append("&quot;");
break;
case '\'':
buffer.append("&apos;");
break;
case '<':
buffer.append("&lt;");
break;
case '>':
buffer.append("&gt;");
break;
default:
buffer.append(&data[pos], 1);
break;
}
}
return buffer;
}
std::string ansiToHtml(const std::string &text) {
// Convert text to HTML-safe first
std::string htmlEscaped = escapeHtml(text);
std::ostringstream oss;
size_t i = 0;
size_t len = htmlEscaped.length();
int openSpans = 0;
auto closeSpans = [&oss, &openSpans]() {
while (openSpans > 0) {
oss << "</span>";
--openSpans;
}
};
while (i < len) {
// Look for ANSI CSI sequence '\033[' or '\x1b['
if ((htmlEscaped[i] == '\033' || htmlEscaped[i] == '\x1b') &&
i + 1 < len && htmlEscaped[i + 1] == '[') {
size_t seqStart = i + 2;
size_t seqEnd = htmlEscaped.find('m', seqStart);
if (seqEnd != std::string::npos) {
std::string codeStr =
htmlEscaped.substr(seqStart, seqEnd - seqStart);
i = seqEnd + 1;
std::istringstream codeStream(codeStr);
std::string codeVal;
// Defaults if sequence is just \033[m (Reset)
if (codeStr.empty()) {
closeSpans();
continue;
}
while (std::getline(codeStream, codeVal, ';')) {
int code = 0;
try {
code = std::stoi(codeVal);
} catch (...) {
continue;
}
switch (code) {
case 0: // Reset
closeSpans();
break;
case 1: // Bold
oss << "<span style='font-weight:bold;'>";
openSpans++;
break;
case 2: // Dim
oss << "<span style='opacity:0.7;'>";
openSpans++;
break;
// Standard Foreground Colors
case 30:
oss << "<span style='color:#2c3e50;'>";
openSpans++;
break; // Black
case 31:
oss << "<span style='color:#e74c3c;'>";
openSpans++;
break; // Red
case 32:
oss << "<span style='color:#27ae60;'>";
openSpans++;
break; // Green
case 33:
oss << "<span style='color:#f39c12;'>";
openSpans++;
break; // Yellow
case 34:
oss << "<span style='color:#2980b9;'>";
openSpans++;
break; // Blue
case 35:
oss << "<span style='color:#8e44ad;'>";
openSpans++;
break; // Magenta
case 36:
oss << "<span style='color:#16a085;'>";
openSpans++;
break; // Cyan
case 37:
oss << "<span style='color:#bdc3c7;'>";
openSpans++;
break; // Light Gray
// Bright Foreground Colors
case 90:
oss << "<span style='color:#7f8c8d;'>";
openSpans++;
break; // Dark Gray
case 91:
oss << "<span style='color:#ff6b6b;'>";
openSpans++;
break; // Bright Red
case 92:
oss << "<span style='color:#51cf66;'>";
openSpans++;
break; // Bright Green
case 93:
oss << "<span style='color:#fcc419;'>";
openSpans++;
break; // Bright Yellow
case 94:
oss << "<span style='color:#339af0;'>";
openSpans++;
break; // Bright Blue
case 95:
oss << "<span style='color:#cc5de8;'>";
openSpans++;
break; // Bright Magenta
case 96:
oss << "<span style='color:#22b8cf;'>";
openSpans++;
break; // Bright Cyan
case 97:
oss << "<span style='color:#ffffff;'>";
openSpans++;
break; // White
default:
break;
}
}
continue;
}
}
oss << htmlEscaped[i];
++i;
}
closeSpans();
return oss.str();
}
std::vector<std::string> wrapText(
const std::string &text,
size_t width
) {
std::vector<std::string> lines;
std::istringstream words(text);
std::string word, line;
while (words >> word) {
if (line.length() + word.length() + 1 > width) {
if (!line.empty()) {
lines.push_back(line);
line.clear();
}
if (word.length() > width) {
lines.push_back(word.substr(0, width - 3) + "...");
continue;
}
}
if (!line.empty())
line += " ";
line += word;
}
if (!line.empty())
lines.push_back(line);
if (lines.empty())
lines.push_back("");
return lines;
}
class CheckReporter : public Catch::StreamingReporterBase {
// Accumulate failure messages for the current test case
struct TestCaseData {
std::string name;
std::string tags;
bool passed;
std::size_t assertionsPassed;
std::size_t assertionsFailed;
std::vector<std::string> failureMessages;
std::vector<std::string> infoMessages;
};
std::vector<std::string> m_currentFailures;
std::vector<std::string> m_currentInfos;
std::vector<TestCaseData> m_testRunData;
public:
using StreamingReporterBase::StreamingReporterBase;
static std::string getDescription() {
return "Fixed-width table reporter with detailed assertion failures.";
return "Console reporter with wrapping, tags, and collapsible HTML "
"export "
"with ANSI color rendering.";
}
void testRunStarting(Catch::TestRunInfo const& _testRunInfo) override {
void testRunStarting(Catch::TestRunInfo const &_testRunInfo) override {
StreamingReporterBase::testRunStarting(_testRunInfo);
std::cout << '\n';
std::cout << std::left << std::setw(55) << "Test Case Name"
<< "Status "
<< std::right << std::setw(8) << "Passed"
std::cout << std::left << std::setw(85) << "Test Case Name"
<< "Status " << std::right << std::setw(8) << "Passed"
<< std::setw(8) << "Failed" << '\n';
std::cout << std::string(81, '-') << '\n';
std::cout << std::string(121, '-') << '\n';
}
// 1. Hook into every assertion to catch failures
void assertionEnded(Catch::AssertionStats const& assertionStats) override {
void assertionEnded(Catch::AssertionStats const &assertionStats) override {
StreamingReporterBase::assertionEnded(assertionStats);
// If the assertion failed, build a detailed message
// Capture INFO messages regardless of pass/fail status
for (auto const &msg : assertionStats.infoMessages) {
m_currentInfos.push_back(msg.message);
}
if (!assertionStats.assertionResult.isOk()) {
auto const& result = assertionStats.assertionResult;
auto const &result = assertionStats.assertionResult;
std::ostringstream oss;
// Format: -> FAILED: [file:line]
oss << " \033[31m-> FAILED:\033[0m "
<< result.getSourceInfo().file << ":" << result.getSourceInfo().line << '\n';
<< result.getSourceInfo().file << ":"
<< result.getSourceInfo().line << '\n';
oss << " " << result.getTestMacroName() << "( "
<< result.getExpression() << " )\n";
// Print the macro used (e.g., REQUIRE, CHECK) and the expression
oss << " " << result.getTestMacroName() << "( " << result.getExpression() << " )\n";
// Print what it actually evaluated to (e.g., 1 == 2)
if (result.hasExpandedExpression()) {
oss << " with expansion:\n"
<< " " << result.getExpandedExpression() << '\n';
}
// Capture any INFO() messages attached to this assertion
for (auto const& msg : assertionStats.infoMessages) {
oss << " info: " << msg.message << '\n';
for (auto const &msg : assertionStats.infoMessages) {
oss << " \033[36m[INFO]\033[0m " << msg.message << '\n';
}
m_currentFailures.push_back(oss.str());
}
}
void testCaseEnded(Catch::TestCaseStats const& stats) override {
void testCaseEnded(Catch::TestCaseStats const &stats) override {
StreamingReporterBase::testCaseEnded(stats);
bool passed = stats.totals.assertions.allPassed();
bool passed = stats.totals.assertions.allPassed();
std::string mark = passed ? "\033[32m✓\033[0m" : "\033[31m✗\033[0m";
std::string name = stats.testInfo->name;
if (name.length() > 53) {
name = name.substr(0, 50) + "...";
auto wrappedName = wrapText(name, 83);
std::cout << std::left << std::setw(85) << wrappedName[0] << mark
<< " " << std::right << std::setw(8)
<< stats.totals.assertions.passed << std::setw(8)
<< stats.totals.assertions.failed << '\n';
for (size_t i = 1; i < wrappedName.size(); ++i) {
std::cout << " \033[90m↳ \033[0m" // Dim indent arrow
<< std::left << std::setw(81) << wrappedName[i] << '\n';
}
// Print the table row
std::cout << std::left << std::setw(55) << name
<< mark << " "
<< std::right << std::setw(8) << stats.totals.assertions.passed
<< std::setw(8) << stats.totals.assertions.failed << '\n';
std::string tagsStr = stats.testInfo->tagsAsString();
if (!tagsStr.empty()) {
auto wrappedTags = wrapText("Tags: " + tagsStr, 83);
for (const auto &line : wrappedTags) {
std::cout << " \033[36m" << line << "\033[0m\n"; // Cyan
}
}
// 2. Print all accumulated failures under the row
if (!m_currentFailures.empty()) {
std::cout << '\n';
for (auto const& failure : m_currentFailures) {
for (auto const &failure : m_currentFailures) {
std::cout << failure << '\n';
}
// Add a separator so multiple failing tests don't blur together
std::cout << std::string(81, '-') << '\n';
// Clear the buffer for the next test case
m_currentFailures.clear();
std::cout << std::string(121, '-') << '\n';
}
m_testRunData.push_back(
{name, tagsStr, passed, stats.totals.assertions.passed,
stats.totals.assertions.failed, m_currentFailures, m_currentInfos}
);
m_currentFailures.clear();
m_currentInfos.clear();
}
void testRunEnded(Catch::TestRunStats const& _testRunStats) override {
void testRunEnded(Catch::TestRunStats const &_testRunStats) override {
StreamingReporterBase::testRunEnded(_testRunStats);
std::cout << std::string(81, '=') << '\n';
std::cout << std::string(121, '=') << '\n';
auto const& tc = _testRunStats.totals.testCases;
auto const& as = _testRunStats.totals.assertions;
auto const &tc = _testRunStats.totals.testCases;
auto const &as = _testRunStats.totals.assertions;
std::string tc_passed_str = tc.passed > 0 ? "\033[32m" + std::to_string(tc.passed) + " passed\033[0m" : "0 passed";
std::string tc_failed_str = tc.failed > 0 ? "\033[31m" + std::to_string(tc.failed) + " failed\033[0m" : "0 failed";
std::string tc_passed_str =
tc.passed > 0
? "\033[32m" + std::to_string(tc.passed) + " passed\033[0m"
: "0 passed";
std::string tc_failed_str =
tc.failed > 0
? "\033[31m" + std::to_string(tc.failed) + " failed\033[0m"
: "0 failed";
std::string as_passed_str = as.passed > 0 ? "\033[32m" + std::to_string(as.passed) + " passed\033[0m" : "0 passed";
std::string as_failed_str = as.failed > 0 ? "\033[31m" + std::to_string(as.failed) + " failed\033[0m" : "0 failed";
std::string as_passed_str =
as.passed > 0
? "\033[32m" + std::to_string(as.passed) + " passed\033[0m"
: "0 passed";
std::string as_failed_str =
as.failed > 0
? "\033[31m" + std::to_string(as.failed) + " failed\033[0m"
: "0 failed";
std::cout << "Test Cases: " << tc_passed_str << ", " << tc_failed_str << ", " << tc.total() << " total\n";
std::cout << "Assertions: " << as_passed_str << ", " << as_failed_str << ", " << as.total() << " total\n\n";
std::cout << "Test Cases: " << tc_passed_str << ", " << tc_failed_str
<< ", " << tc.total() << " total\n";
std::cout << "Assertions: " << as_passed_str << ", " << as_failed_str
<< ", " << as.total() << " total\n\n";
generateHtmlReport(_testRunStats);
}
private:
void generateHtmlReport(Catch::TestRunStats const &stats) {
std::ofstream html("test_summary.html");
if (!html)
return;
html
<< "<!DOCTYPE html>\n<html lang='en'>\n<head>\n"
<< "<meta charset='UTF-8'>\n"
<< "<meta name='viewport' content='width=device-width, "
"initial-scale=1.0'>\n"
<< "<title>Test Run Summary</title>\n"
<< "<style>\n"
<< "body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe "
"UI', "
"Roboto, Helvetica, Arial, sans-serif; "
"background: #f4f6f8; color: #333; margin: 0; padding: 2rem; }\n"
<< "h1 { color: #2c3e50; border-bottom: 2px solid #e0e0e0; "
"padding-bottom: 0.5rem; }\n"
<< ".summary-cards { display: flex; gap: 1rem; margin-bottom: "
"2rem; }\n"
<< ".card { background: white; padding: 1rem 1.5rem; "
"border-radius: "
"8px; box-shadow: 0 2px 4px "
"rgba(0,0,0,0.05); flex: 1; }\n"
<< ".card h3 { margin-top: 0; font-size: 0.9rem; color: #7f8c8d; "
"text-transform: uppercase; }\n"
<< ".card p { font-size: 1.5rem; font-weight: bold; margin: 0; }\n"
<< ".text-green { color: #27ae60; }\n"
<< ".text-red { color: #e74c3c; }\n"
<< ".test-item { background: white; border-radius: 8px; padding: "
"1rem; "
"margin-bottom: 1rem; box-shadow: 0 2px "
"4px rgba(0,0,0,0.05); border-left: 5px solid #bdc3c7; }\n"
<< ".test-item.passed { border-left-color: #27ae60; }\n"
<< ".test-item.failed { border-left-color: #e74c3c; }\n"
<< ".test-header { display: flex; justify-content: space-between; "
"align-items: flex-start; }\n"
<< ".test-name { font-size: 1.1rem; font-weight: 600; margin: 0 0 "
"0.5rem 0; word-break: break-word; }\n"
<< ".tags { font-size: 0.8rem; color: #2980b9; background: "
"#ebf5fb; "
"padding: 2px 6px; border-radius: 4px; "
"display: inline-block; margin-top: 4px; }\n"
<< ".stats { font-size: 0.9rem; color: #7f8c8d; }\n"
<< "details { margin-top: 0.8rem; background: #f8f9fa; border: 1px "
"solid #e9ecef; border-radius: 6px; "
"padding: 0.5rem 0.8rem; }\n"
<< "summary { cursor: pointer; font-weight: 600; color: #34495e; "
"user-select: none; font-size: 0.9rem; }\n"
<< "summary:hover { color: #2980b9; }\n"
<< "pre { background: #1e293b; color: #f8fafc; padding: 1rem; "
"border-radius: 4px; overflow-x: auto; "
"font-size: 0.85rem; line-height: 1.4; margin-top: 0.5rem; }\n"
<< "pre.info-block { background: #0f172a; border-left: 4px solid "
"#0284c7; }\n"
<< "</style>\n</head>\n<body>\n";
html << "<h1>Test Run Summary</h1>\n";
// Summary Cards
html << "<div class='summary-cards'>\n";
html << "<div class='card'><h3>Total Cases</h3><p>"
<< stats.totals.testCases.total() << "</p></div>\n";
html << "<div class='card'><h3>Cases Passed</h3><p class='text-green'>"
<< stats.totals.testCases.passed << "</p></div>\n";
html << "<div class='card'><h3>Cases Failed</h3><p class='text-red'>"
<< stats.totals.testCases.failed << "</p></div>\n";
html << "</div>\n";
for (const auto &test : m_testRunData) {
std::string statusClass = test.passed ? "passed" : "failed";
html << "<div class='test-item " << statusClass << "'>\n";
html << " <div class='test-header'>\n";
html << " <div>\n";
html << " <h3 class='test-name'>" << escapeHtml(test.name)
<< "</h3>\n";
if (!test.tags.empty()) {
html << " <div class='tags'>" << escapeHtml(test.tags)
<< "</div>\n";
}
html << " </div>\n";
html << " <div class='stats'>\n";
html << " <span class='text-green'>&#10003; "
<< test.assertionsPassed << "</span> | ";
html << " <span class='text-red'>&#10007; "
<< test.assertionsFailed << "</span>\n";
html << " </div>\n";
html << " </div>\n";
// Collapsible INFO Messages section with ANSI color rendering
if (!test.infoMessages.empty()) {
html << " <details>\n";
html << " <summary>Info Logs (" << test.infoMessages.size()
<< ")</summary>\n";
html << " <pre class='info-block'>";
for (const auto &info : test.infoMessages) {
html << "[INFO] " << ansiToHtml(info) << "\n";
}
html << "</pre>\n";
html << " </details>\n";
}
// Collapsible Failures section with ANSI color rendering
if (!test.failureMessages.empty()) {
html << " <details open>\n";
html << " <summary class='text-red'>Failure Details ("
<< test.failureMessages.size() << ")</summary>\n";
html << " <pre>";
for (const auto &msg : test.failureMessages) {
html << ansiToHtml(msg) << "\n";
}
html << " </pre>\n";
html << " </details>\n";
}
html << "</div>\n";
}
html << "</body>\n</html>\n";
}
};
CATCH_REGISTER_REPORTER("check", CheckReporter)
int main(int argc, char* argv[]) {
CATCH_REGISTER_REPORTER(
"check",
CheckReporter
)
int main(
int argc,
char *argv[]
) {
fourdst::config::Config<mean_field::utils::Args> cfg;
CLI::App app{"Mean Field Tests"};
@@ -148,49 +517,56 @@ int main(int argc, char* argv[]) {
}
}
std::vector<const char*> config_argv;
std::vector<const char *> config_argv;
config_argv.reserve(config_arguments.size());
for (const std::string& argument : config_arguments) {
for (const std::string &argument : config_arguments) {
config_argv.push_back(argument.c_str());
}
try {
app.parse(static_cast<int>(config_argv.size()), config_argv.data());
} catch (const CLI::ParseError& error) {
} catch (const CLI::ParseError &error) {
return app.exit(error);
}
std::vector<std::string> catch_arguments;
catch_arguments.emplace_back(argv[0]);
for (const std::string& argument : app.remaining()) {
for (const std::string &argument : app.remaining()) {
catch_arguments.push_back(argument);
}
for (const std::string& argument : forced_catch_arguments) {
for (const std::string &argument : forced_catch_arguments) {
catch_arguments.push_back(argument);
}
const auto is_reporter_option = [](const std::string& argument) {
return argument == "-r" || argument == "--reporter" || argument.starts_with("-r=") || argument.starts_with("--reporter=");
const auto is_reporter_option = [](const std::string &argument) {
return argument == "-r" || argument == "--reporter" ||
argument.starts_with("-r=") ||
argument.starts_with("--reporter=");
};
if (const bool has_reporter = std::ranges::any_of(catch_arguments, is_reporter_option); !has_reporter) {
if (const bool has_reporter =
std::ranges::any_of(catch_arguments, is_reporter_option);
!has_reporter) {
catch_arguments.emplace_back("--reporter");
catch_arguments.emplace_back("check");
}
std::vector<const char*> catch_argv;
std::vector<const char *> catch_argv;
catch_argv.reserve(catch_arguments.size());
for (const std::string& argument : catch_arguments) {
for (const std::string &argument : catch_arguments) {
catch_argv.push_back(argument.c_str());
}
Catch::Session session;
if (const int catch_parse_result = session.applyCommandLine(static_cast<int>(catch_argv.size()), catch_argv.data()); catch_parse_result != 0) {
if (const int catch_parse_result = session.applyCommandLine(
static_cast<int>(catch_argv.size()), catch_argv.data()
);
catch_parse_result != 0) {
return catch_parse_result;
}
@@ -201,7 +577,8 @@ int main(int argc, char* argv[]) {
const int hdiv_max_q1d = mfem::DeviceDofQuadLimits::Get().HDIV_MAX_Q1D;
std::cout << "H(div) maximum Q1D = " << hdiv_max_q1d << '\n';
std::cout << "Approximate maximum safe integration order = " << 2 * hdiv_max_q1d - 1 << '\n';
std::cout << "Approximate maximum safe integration order = "
<< 2 * hdiv_max_q1d - 1 << '\n';
mean_field::utils::Args test_args = cfg.main();

861
tests/utils/blocks.cpp Normal file
View File

@@ -0,0 +1,861 @@
#include <array>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
#include <type_traits>
import mean_field;
import test_helpers;
using namespace mean_field;
namespace {
namespace blocks = utils::blocks;
template <typename Query, typename List> struct contains_type;
template <typename Query>
struct contains_type<Query, blocks::type_list<>> : std::false_type { };
template <typename Query, typename Head, typename... Tail>
struct contains_type<Query, blocks::type_list<Head, Tail...>>
: std::conditional_t<
std::is_same_v<Query, Head>,
std::true_type,
contains_type<Query, blocks::type_list<Tail...>>> { };
template <typename Query, typename List>
inline constexpr bool contains_type_v = contains_type<Query, List>::value;
template <int index, typename List> struct type_at;
template <typename Head, typename... Tail>
struct type_at<0, blocks::type_list<Head, Tail...>> {
using type = Head;
};
template <int index, typename Head, typename... Tail>
struct type_at<index, blocks::type_list<Head, Tail...>> {
static_assert(index > 0);
using type =
typename type_at<index - 1, blocks::type_list<Tail...>>::type;
};
template <int index, typename List>
using type_at_t = typename type_at<index, List>::type;
template <typename Row> struct block_row_traits;
template <typename Residual, typename... Values>
struct block_row_traits<blocks::block_row<Residual, Values...>> {
using residual = Residual;
using values = blocks::type_list<Values...>;
static constexpr int value_count = sizeof...(Values);
};
template <typename Row, typename... ExpectedValues>
inline constexpr bool row_has_exact_values_v =
block_row_traits<Row>::value_count == sizeof...(ExpectedValues) &&
(contains_type_v<
ExpectedValues,
typename block_row_traits<Row>::values> &&
...);
struct foreign_value final : blocks::value_block_base { };
struct foreign_residual final : blocks::residual_block_base { };
} // namespace
TEST_CASE(
"Block Types Preserve Their Semantic Hierarchy",
tags::unit &tags::solver &tags::utils
) {
STATIC_REQUIRE(
std::is_base_of_v<blocks::block, blocks::residual_block_base>
);
STATIC_REQUIRE(std::is_base_of_v<blocks::block, blocks::value_block_base>);
STATIC_REQUIRE(
std::is_base_of_v<
blocks::residual_block_base, blocks::residual_block<0>>
);
STATIC_REQUIRE(
std::is_base_of_v<blocks::value_block_base, blocks::value_block<0>>
);
STATIC_REQUIRE(std::is_base_of_v<blocks::field, blocks::density>);
STATIC_REQUIRE(std::is_base_of_v<blocks::field, blocks::displacement>);
STATIC_REQUIRE(std::is_base_of_v<blocks::field, blocks::gravity>);
STATIC_REQUIRE(std::is_base_of_v<blocks::field, blocks::enthalpy>);
STATIC_REQUIRE(std::is_base_of_v<blocks::term, blocks::density::mass>);
STATIC_REQUIRE(
std::is_base_of_v<blocks::term, blocks::displacement::geometry>
);
STATIC_REQUIRE(std::is_base_of_v<blocks::term, blocks::gravity::gradient>);
STATIC_REQUIRE(std::is_base_of_v<blocks::term, blocks::gravity::poisson>);
STATIC_REQUIRE(std::is_base_of_v<blocks::term, blocks::enthalpy::specific>);
STATIC_REQUIRE(
std::is_base_of_v<
blocks::value_block_base, blocks::density::mass::value>
);
STATIC_REQUIRE(
std::is_base_of_v<
blocks::residual_block_base, blocks::density::mass::residual>
);
STATIC_REQUIRE(
std::is_base_of_v<
blocks::value_block_base, blocks::gravity::gradient::value>
);
STATIC_REQUIRE(
std::is_base_of_v<
blocks::residual_block_base, blocks::gravity::gradient::residual>
);
STATIC_REQUIRE(
std::is_base_of_v<
blocks::value_block_base, blocks::gravity::poisson::value>
);
STATIC_REQUIRE(
std::is_base_of_v<
blocks::residual_block_base, blocks::gravity::poisson::residual>
);
STATIC_REQUIRE(
std::is_base_of_v<
blocks::value_block_base, blocks::enthalpy::specific::value>
);
STATIC_REQUIRE(
std::is_base_of_v<
blocks::residual_block_base, blocks::enthalpy::specific::residual>
);
STATIC_REQUIRE(std::is_empty_v<blocks::density>);
STATIC_REQUIRE(std::is_empty_v<blocks::displacement>);
STATIC_REQUIRE(std::is_empty_v<blocks::gravity>);
STATIC_REQUIRE(std::is_empty_v<blocks::enthalpy>);
STATIC_REQUIRE(std::is_empty_v<blocks::density::mass>);
STATIC_REQUIRE(std::is_empty_v<blocks::gravity::gradient>);
STATIC_REQUIRE(std::is_empty_v<blocks::gravity::poisson>);
STATIC_REQUIRE(std::is_empty_v<blocks::enthalpy::specific>);
STATIC_REQUIRE(
std::is_base_of_v<blocks::field, blocks::barotropic_constant>
);
STATIC_REQUIRE(
std::is_base_of_v<
blocks::term, blocks::barotropic_constant::mass_normalization>
);
STATIC_REQUIRE(
std::is_base_of_v<
blocks::value_block_base,
blocks::barotropic_constant::mass_normalization::value>
);
STATIC_REQUIRE(
std::is_base_of_v<
blocks::residual_block_base,
blocks::barotropic_constant::mass_normalization::residual>
);
STATIC_REQUIRE(
blocks::barotropic_constant::mass_normalization::value::
static_block_size == 1
);
STATIC_REQUIRE(
blocks::barotropic_constant::mass_normalization::residual::
static_block_size == 1
);
STATIC_REQUIRE(std::is_empty_v<blocks::barotropic_constant>);
STATIC_REQUIRE(
std::is_empty_v<blocks::barotropic_constant::mass_normalization>
);
CHECK(true);
}
TEST_CASE(
"Type Lists Resolve Their Types At Compile Time",
tags::unit &tags::solver &tags::utils
) {
using list = blocks::type_list<
blocks::density::mass::value, blocks::displacement::geometry::value,
blocks::gravity::gradient::value, blocks::gravity::poisson::value>;
STATIC_REQUIRE(list::size == 4);
STATIC_REQUIRE(
blocks::type_index_v<blocks::density::mass::value, list> == 0
);
STATIC_REQUIRE(
blocks::type_index_v<blocks::displacement::geometry::value, list> == 1
);
STATIC_REQUIRE(
blocks::type_index_v<blocks::gravity::gradient::value, list> == 2
);
STATIC_REQUIRE(
blocks::type_index_v<blocks::gravity::poisson::value, list> == 3
);
STATIC_REQUIRE(
std::is_same_v<type_at_t<0, list>, blocks::density::mass::value>
);
STATIC_REQUIRE(
std::is_same_v<
type_at_t<1, list>, blocks::displacement::geometry::value>
);
STATIC_REQUIRE(
std::is_same_v<type_at_t<2, list>, blocks::gravity::gradient::value>
);
STATIC_REQUIRE(
std::is_same_v<type_at_t<3, list>, blocks::gravity::poisson::value>
);
CHECK(true);
}
TEST_CASE(
"Gravity Field Form Resolves Value And Residual Blocks",
tags::unit &tags::solver &tags::utils
) {
using form = blocks::gravity_field_form;
constexpr auto density_value =
blocks::get_value_block<form>(blocks::density_field.mass_term);
constexpr auto displacement_value =
blocks::get_value_block<form>(blocks::displacement_field.geometry_term);
constexpr auto gravity_gradient_value =
blocks::get_value_block<form>(blocks::gravity_field.gradient_term);
constexpr auto gravity_potential_value =
blocks::get_value_block<form>(blocks::gravity_field.poisson_term);
constexpr auto gravity_gradient_residual =
blocks::get_residual_block<form>(blocks::gravity_field.gradient_term);
constexpr auto gravity_poisson_residual =
blocks::get_residual_block<form>(blocks::gravity_field.poisson_term);
STATIC_REQUIRE(form::value_block_count == 4);
STATIC_REQUIRE(form::residual_block_count == 2);
STATIC_REQUIRE(
std::is_same_v<
std::remove_cv_t<decltype(density_value)>, blocks::value_block<0>>
);
STATIC_REQUIRE(
std::is_same_v<
std::remove_cv_t<decltype(displacement_value)>,
blocks::value_block<1>>
);
STATIC_REQUIRE(
std::is_same_v<
std::remove_cv_t<decltype(gravity_gradient_value)>,
blocks::value_block<2>>
);
STATIC_REQUIRE(
std::is_same_v<
std::remove_cv_t<decltype(gravity_potential_value)>,
blocks::value_block<3>>
);
STATIC_REQUIRE(
std::is_same_v<
std::remove_cv_t<decltype(gravity_gradient_residual)>,
blocks::residual_block<0>>
);
STATIC_REQUIRE(
std::is_same_v<
std::remove_cv_t<decltype(gravity_poisson_residual)>,
blocks::residual_block<1>>
);
CHECK(static_cast<int>(density_value) == 0);
CHECK(static_cast<int>(displacement_value) == 1);
CHECK(static_cast<int>(gravity_gradient_value) == 2);
CHECK(static_cast<int>(gravity_potential_value) == 3);
CHECK(static_cast<int>(gravity_gradient_residual) == 0);
CHECK(static_cast<int>(gravity_poisson_residual) == 1);
}
TEST_CASE(
"Resolved Blocks Implicitly Convert For MFEM Interfaces",
tags::unit &tags::solver &tags::utils
) {
using form = blocks::gravity_field_form;
constexpr auto density_value =
blocks::get_value_block<form>(blocks::density_field.mass_term);
constexpr auto gravity_potential_value =
blocks::get_value_block<form>(blocks::gravity_field.poisson_term);
constexpr auto gravity_gradient_residual =
blocks::get_residual_block<form>(blocks::gravity_field.gradient_term);
constexpr auto gravity_poisson_residual =
blocks::get_residual_block<form>(blocks::gravity_field.poisson_term);
auto consume_block_index = [](const int block_index) {
return block_index;
};
CHECK(consume_block_index(density_value) == 0);
CHECK(consume_block_index(gravity_potential_value) == 3);
CHECK(consume_block_index(gravity_gradient_residual) == 0);
CHECK(consume_block_index(gravity_poisson_residual) == 1);
}
TEST_CASE(
"Block Indices Belong To Their Form",
tags::unit &tags::solver &tags::utils
) {
using reordered_form = blocks::block_form<
blocks::type_list<
blocks::gravity::poisson::value, blocks::gravity::gradient::value,
blocks::density::mass::value,
blocks::displacement::geometry::value>,
blocks::type_list<
blocks::gravity::poisson::residual,
blocks::gravity::gradient::residual>>;
constexpr auto gravity_potential_value =
blocks::get_value_block<reordered_form>(
blocks::gravity_field.poisson_term
);
constexpr auto gravity_gradient_value =
blocks::get_value_block<reordered_form>(
blocks::gravity_field.gradient_term
);
constexpr auto density_value = blocks::get_value_block<reordered_form>(
blocks::density_field.mass_term
);
constexpr auto displacement_value = blocks::get_value_block<reordered_form>(
blocks::displacement_field.geometry_term
);
constexpr auto gravity_poisson_residual =
blocks::get_residual_block<reordered_form>(
blocks::gravity_field.poisson_term
);
constexpr auto gravity_gradient_residual =
blocks::get_residual_block<reordered_form>(
blocks::gravity_field.gradient_term
);
STATIC_REQUIRE(static_cast<int>(gravity_potential_value) == 0);
STATIC_REQUIRE(static_cast<int>(gravity_gradient_value) == 1);
STATIC_REQUIRE(static_cast<int>(density_value) == 2);
STATIC_REQUIRE(static_cast<int>(displacement_value) == 3);
STATIC_REQUIRE(static_cast<int>(gravity_poisson_residual) == 0);
STATIC_REQUIRE(static_cast<int>(gravity_gradient_residual) == 1);
CHECK(true);
}
TEST_CASE(
"Form Layout Constructs Runtime Offsets From Block Sizes",
tags::unit &tags::solver &tags::utils
) {
using form = blocks::gravity_field_form;
const std::array<int, form::value_block_count> value_sizes{11, 13, 17, 19};
const std::array<int, form::residual_block_count> residual_sizes{23, 29};
const blocks::form_layout<form> layout(value_sizes, residual_sizes);
constexpr auto density_value =
blocks::get_value_block<form>(blocks::density_field.mass_term);
constexpr auto displacement_value =
blocks::get_value_block<form>(blocks::displacement_field.geometry_term);
constexpr auto gravity_gradient_value =
blocks::get_value_block<form>(blocks::gravity_field.gradient_term);
constexpr auto gravity_potential_value =
blocks::get_value_block<form>(blocks::gravity_field.poisson_term);
constexpr auto gravity_gradient_residual =
blocks::get_residual_block<form>(blocks::gravity_field.gradient_term);
constexpr auto gravity_poisson_residual =
blocks::get_residual_block<form>(blocks::gravity_field.poisson_term);
CHECK(layout.size(density_value) == 11);
CHECK(layout.size(displacement_value) == 13);
CHECK(layout.size(gravity_gradient_value) == 17);
CHECK(layout.size(gravity_potential_value) == 19);
CHECK(layout.size(gravity_gradient_residual) == 23);
CHECK(layout.size(gravity_poisson_residual) == 29);
CHECK(layout.offset(density_value) == 0);
CHECK(layout.offset(displacement_value) == 11);
CHECK(layout.offset(gravity_gradient_value) == 24);
CHECK(layout.offset(gravity_potential_value) == 41);
CHECK(layout.offset(gravity_gradient_residual) == 0);
CHECK(layout.offset(gravity_poisson_residual) == 23);
REQUIRE(layout.value_offsets().Size() == 5);
CHECK(layout.value_offsets()[0] == 0);
CHECK(layout.value_offsets()[1] == 11);
CHECK(layout.value_offsets()[2] == 24);
CHECK(layout.value_offsets()[3] == 41);
CHECK(layout.value_offsets()[4] == 60);
REQUIRE(layout.residual_offsets().Size() == 3);
CHECK(layout.residual_offsets()[0] == 0);
CHECK(layout.residual_offsets()[1] == 23);
CHECK(layout.residual_offsets()[2] == 52);
}
TEST_CASE(
"Form Layout Supports Empty Runtime Blocks",
tags::unit &tags::solver &tags::utils
) {
using form = blocks::gravity_field_form;
const std::array<int, form::value_block_count> value_sizes{3, 0, 5, 0};
const std::array<int, form::residual_block_count> residual_sizes{0, 7};
const blocks::form_layout<form> layout(value_sizes, residual_sizes);
constexpr auto density_value =
blocks::get_value_block<form>(blocks::density_field.mass_term);
constexpr auto displacement_value =
blocks::get_value_block<form>(blocks::displacement_field.geometry_term);
constexpr auto gravity_gradient_value =
blocks::get_value_block<form>(blocks::gravity_field.gradient_term);
constexpr auto gravity_potential_value =
blocks::get_value_block<form>(blocks::gravity_field.poisson_term);
constexpr auto gravity_gradient_residual =
blocks::get_residual_block<form>(blocks::gravity_field.gradient_term);
constexpr auto gravity_poisson_residual =
blocks::get_residual_block<form>(blocks::gravity_field.poisson_term);
CHECK(layout.size(density_value) == 3);
CHECK(layout.size(displacement_value) == 0);
CHECK(layout.size(gravity_gradient_value) == 5);
CHECK(layout.size(gravity_potential_value) == 0);
CHECK(layout.size(gravity_gradient_residual) == 0);
CHECK(layout.size(gravity_poisson_residual) == 7);
CHECK(layout.offset(density_value) == 0);
CHECK(layout.offset(displacement_value) == 3);
CHECK(layout.offset(gravity_gradient_value) == 3);
CHECK(layout.offset(gravity_potential_value) == 8);
CHECK(layout.offset(gravity_gradient_residual) == 0);
CHECK(layout.offset(gravity_poisson_residual) == 0);
CHECK(layout.value_offsets().Last() == 8);
CHECK(layout.residual_offsets().Last() == 7);
}
TEST_CASE(
"Form Layout Integrates With MFEM Block Vectors",
tags::unit &tags::solver &tags::utils
) {
using form = blocks::gravity_field_form;
const std::array<int, form::value_block_count> value_sizes{3, 5, 7, 11};
const std::array<int, form::residual_block_count> residual_sizes{13, 17};
const blocks::form_layout<form> layout(value_sizes, residual_sizes);
constexpr auto density_value =
blocks::get_value_block<form>(blocks::density_field.mass_term);
constexpr auto displacement_value =
blocks::get_value_block<form>(blocks::displacement_field.geometry_term);
constexpr auto gravity_gradient_value =
blocks::get_value_block<form>(blocks::gravity_field.gradient_term);
constexpr auto gravity_potential_value =
blocks::get_value_block<form>(blocks::gravity_field.poisson_term);
constexpr auto gravity_gradient_residual =
blocks::get_residual_block<form>(blocks::gravity_field.gradient_term);
constexpr auto gravity_poisson_residual =
blocks::get_residual_block<form>(blocks::gravity_field.poisson_term);
mfem::BlockVector values(layout.value_offsets());
mfem::BlockVector residuals(layout.residual_offsets());
values = 0.0;
residuals = 0.0;
values.GetBlock(density_value) = 1.0;
values.GetBlock(displacement_value) = 2.0;
values.GetBlock(gravity_gradient_value) = 3.0;
values.GetBlock(gravity_potential_value) = 4.0;
residuals.GetBlock(gravity_gradient_residual) = 5.0;
residuals.GetBlock(gravity_poisson_residual) = 6.0;
CHECK(values.GetBlock(density_value).Size() == 3);
CHECK(values.GetBlock(displacement_value).Size() == 5);
CHECK(values.GetBlock(gravity_gradient_value).Size() == 7);
CHECK(values.GetBlock(gravity_potential_value).Size() == 11);
CHECK(residuals.GetBlock(gravity_gradient_residual).Size() == 13);
CHECK(residuals.GetBlock(gravity_poisson_residual).Size() == 17);
CHECK(values.GetBlock(density_value).Min() == 1.0);
CHECK(values.GetBlock(density_value).Max() == 1.0);
CHECK(values.GetBlock(displacement_value).Min() == 2.0);
CHECK(values.GetBlock(displacement_value).Max() == 2.0);
CHECK(values.GetBlock(gravity_gradient_value).Min() == 3.0);
CHECK(values.GetBlock(gravity_gradient_value).Max() == 3.0);
CHECK(values.GetBlock(gravity_potential_value).Min() == 4.0);
CHECK(values.GetBlock(gravity_potential_value).Max() == 4.0);
CHECK(residuals.GetBlock(gravity_gradient_residual).Min() == 5.0);
CHECK(residuals.GetBlock(gravity_gradient_residual).Max() == 5.0);
CHECK(residuals.GetBlock(gravity_poisson_residual).Min() == 6.0);
CHECK(residuals.GetBlock(gravity_poisson_residual).Max() == 6.0);
}
TEST_CASE(
"Compile Time Blocks Configure An MFEM Block Operator",
tags::unit &tags::solver &tags::utils
) {
using form = blocks::gravity_field_form;
const std::array<int, form::value_block_count> value_sizes{3, 5, 7, 11};
const std::array<int, form::residual_block_count> residual_sizes{13, 17};
const blocks::form_layout<form> layout(value_sizes, residual_sizes);
constexpr auto density_value =
blocks::get_value_block<form>(blocks::density_field.mass_term);
constexpr auto gravity_gradient_residual =
blocks::get_residual_block<form>(blocks::gravity_field.gradient_term);
constexpr auto gravity_poisson_residual =
blocks::get_residual_block<form>(blocks::gravity_field.poisson_term);
mfem::DenseMatrix source_block(
layout.size(gravity_poisson_residual), layout.size(density_value)
);
source_block = 1.0;
mfem::BlockOperator block_operator(
layout.residual_offsets(), layout.value_offsets()
);
block_operator.SetBlock(
gravity_poisson_residual, density_value, &source_block
);
mfem::BlockVector values(layout.value_offsets());
mfem::BlockVector residuals(layout.residual_offsets());
values = 0.0;
residuals = 0.0;
values.GetBlock(density_value) = 2.0;
block_operator.Mult(values, residuals);
CHECK(residuals.GetBlock(gravity_gradient_residual).Norml2() == 0.0);
const mfem::Vector &poisson_residual =
residuals.GetBlock(gravity_poisson_residual);
REQUIRE(poisson_residual.Size() == residual_sizes[1]);
for (int i = 0; i < poisson_residual.Size(); ++i) {
CHECK(poisson_residual(i) == 2.0 * value_sizes[0]);
}
}
TEST_CASE(
"Gravity Jacobian Form Describes The Expected Couplings",
tags::unit &tags::solver &tags::utils
) {
using gradient_row = type_at_t<0, blocks::gravity_jacobian_form>;
using poisson_row = type_at_t<1, blocks::gravity_jacobian_form>;
using gradient_traits = block_row_traits<gradient_row>;
using poisson_traits = block_row_traits<poisson_row>;
STATIC_REQUIRE(blocks::gravity_jacobian_form::size == 2);
STATIC_REQUIRE(
std::is_same_v<
typename gradient_traits::residual,
blocks::gravity::gradient::residual>
);
STATIC_REQUIRE(gradient_traits::value_count == 3);
STATIC_REQUIRE(
contains_type_v<
blocks::gravity::gradient::value, typename gradient_traits::values>
);
STATIC_REQUIRE(
contains_type_v<
blocks::gravity::poisson::value, typename gradient_traits::values>
);
STATIC_REQUIRE(
contains_type_v<
blocks::displacement::geometry::value,
typename gradient_traits::values>
);
STATIC_REQUIRE_FALSE(
contains_type_v<
blocks::density::mass::value, typename gradient_traits::values>
);
STATIC_REQUIRE(
std::is_same_v<
typename poisson_traits::residual,
blocks::gravity::poisson::residual>
);
STATIC_REQUIRE(poisson_traits::value_count == 3);
STATIC_REQUIRE(
contains_type_v<
blocks::gravity::gradient::value, typename poisson_traits::values>
);
STATIC_REQUIRE(
contains_type_v<
blocks::density::mass::value, typename poisson_traits::values>
);
STATIC_REQUIRE(
contains_type_v<
blocks::displacement::geometry::value,
typename poisson_traits::values>
);
STATIC_REQUIRE_FALSE(
contains_type_v<
blocks::gravity::poisson::value, typename poisson_traits::values>
);
CHECK(true);
}
TEST_CASE(
"Barotropic Equilibrium Form Encodes The Agreed Row And Column Layouts",
tags::unit &tags::solver &tags::utils
) {
using form = blocks::barotropic_equilibrium_form;
constexpr auto density_value =
blocks::get_value_block<form>(blocks::density_field.mass_term);
constexpr auto displacement_value =
blocks::get_value_block<form>(blocks::displacement_field.geometry_term);
constexpr auto gravity_gradient_value =
blocks::get_value_block<form>(blocks::gravity_field.gradient_term);
constexpr auto gravity_potential_value =
blocks::get_value_block<form>(blocks::gravity_field.poisson_term);
constexpr auto enthalpy_value =
blocks::get_value_block<form>(blocks::enthalpy_field.specific_term);
constexpr auto barotropic_constant_value = blocks::get_value_block<form>(
blocks::barotropic_constant_field.mass_normalization_term
);
constexpr auto gravity_gradient_residual =
blocks::get_residual_block<form>(blocks::gravity_field.gradient_term);
constexpr auto gravity_poisson_residual =
blocks::get_residual_block<form>(blocks::gravity_field.poisson_term);
constexpr auto density_residual =
blocks::get_residual_block<form>(blocks::density_field.mass_term);
constexpr auto displacement_residual = blocks::get_residual_block<form>(
blocks::displacement_field.geometry_term
);
constexpr auto enthalpy_residual =
blocks::get_residual_block<form>(blocks::enthalpy_field.specific_term);
constexpr auto mass_residual = blocks::get_residual_block<form>(
blocks::barotropic_constant_field.mass_normalization_term
);
STATIC_REQUIRE(form::value_block_count == 6);
STATIC_REQUIRE(form::residual_block_count == 6);
STATIC_REQUIRE(static_cast<int>(density_value) == 0);
STATIC_REQUIRE(static_cast<int>(displacement_value) == 1);
STATIC_REQUIRE(static_cast<int>(gravity_gradient_value) == 2);
STATIC_REQUIRE(static_cast<int>(gravity_potential_value) == 3);
STATIC_REQUIRE(static_cast<int>(enthalpy_value) == 4);
STATIC_REQUIRE(static_cast<int>(barotropic_constant_value) == 5);
STATIC_REQUIRE(static_cast<int>(gravity_gradient_residual) == 0);
STATIC_REQUIRE(static_cast<int>(gravity_poisson_residual) == 1);
STATIC_REQUIRE(static_cast<int>(density_residual) == 2);
STATIC_REQUIRE(static_cast<int>(displacement_residual) == 3);
STATIC_REQUIRE(static_cast<int>(enthalpy_residual) == 4);
STATIC_REQUIRE(static_cast<int>(mass_residual) == 5);
CHECK(true);
}
TEST_CASE(
"Barotropic Jacobian Form Encodes The Exact Direct Coupling Graph",
tags::unit &tags::solver &tags::utils
) {
using jacobian = blocks::barotropic_equilibrium_jacobian_form;
using gradient_row = type_at_t<0, jacobian>;
using poisson_row = type_at_t<1, jacobian>;
using density_row = type_at_t<2, jacobian>;
using displacement_row = type_at_t<3, jacobian>;
using enthalpy_row = type_at_t<4, jacobian>;
using mass_row = type_at_t<5, jacobian>;
STATIC_REQUIRE(jacobian::size == 6);
STATIC_REQUIRE(
row_has_exact_values_v<
gradient_row, blocks::gravity::gradient::value,
blocks::gravity::poisson::value,
blocks::displacement::geometry::value>
);
STATIC_REQUIRE(
row_has_exact_values_v<
poisson_row, blocks::gravity::gradient::value,
blocks::density::mass::value, blocks::displacement::geometry::value>
);
STATIC_REQUIRE(
row_has_exact_values_v<
density_row, blocks::density::mass::value,
blocks::enthalpy::specific::value,
blocks::displacement::geometry::value>
);
STATIC_REQUIRE(
row_has_exact_values_v<
displacement_row, blocks::displacement::geometry::value,
blocks::enthalpy::specific::value>
);
STATIC_REQUIRE(
row_has_exact_values_v<
enthalpy_row, blocks::enthalpy::specific::value,
blocks::gravity::poisson::value,
blocks::displacement::geometry::value,
blocks::barotropic_constant::mass_normalization::value>
);
STATIC_REQUIRE(
row_has_exact_values_v<
mass_row, blocks::density::mass::value,
blocks::displacement::geometry::value>
);
STATIC_REQUIRE_FALSE(
blocks::has_jacobian_coupling_v<
blocks::displacement::geometry::residual,
blocks::gravity::poisson::value, jacobian>
);
STATIC_REQUIRE_FALSE(
blocks::has_jacobian_coupling_v<
blocks::barotropic_constant::mass_normalization::residual,
blocks::barotropic_constant::mass_normalization::value, jacobian>
);
STATIC_REQUIRE_FALSE(
blocks::has_jacobian_coupling_v<
blocks::barotropic_constant::mass_normalization::residual,
blocks::enthalpy::specific::value, jacobian>
);
CHECK(true);
}
TEST_CASE(
"Block Validators Reject Structurally Malformed Forms",
tags::unit &tags::solver &tags::utils
) {
using form = blocks::barotropic_equilibrium_form;
using jacobian = blocks::barotropic_equilibrium_jacobian_form;
using gradient_row = type_at_t<0, jacobian>;
using poisson_row = type_at_t<1, jacobian>;
using density_row = type_at_t<2, jacobian>;
using displacement_row = type_at_t<3, jacobian>;
using enthalpy_row = type_at_t<4, jacobian>;
using mass_row = type_at_t<5, jacobian>;
using missing_row = blocks::type_list<
gradient_row, poisson_row, density_row, displacement_row, enthalpy_row>;
using duplicate_row = blocks::type_list<
gradient_row, poisson_row, density_row, displacement_row, enthalpy_row,
enthalpy_row>;
using reordered_rows = blocks::type_list<
poisson_row, gradient_row, density_row, displacement_row, enthalpy_row,
mass_row>;
using foreign_value_row = blocks::block_row<
blocks::enthalpy::specific::residual, blocks::enthalpy::specific::value,
blocks::gravity::poisson::value, blocks::displacement::geometry::value,
foreign_value>;
using foreign_value_jacobian = blocks::type_list<
gradient_row, poisson_row, density_row, displacement_row,
foreign_value_row, mass_row>;
using duplicate_value_row = blocks::block_row<
blocks::enthalpy::specific::residual, blocks::enthalpy::specific::value,
blocks::gravity::poisson::value, blocks::displacement::geometry::value,
blocks::barotropic_constant::mass_normalization::value,
blocks::barotropic_constant::mass_normalization::value>;
using duplicate_value_jacobian = blocks::type_list<
gradient_row, poisson_row, density_row, displacement_row,
duplicate_value_row, mass_row>;
using unknown_residual_row =
blocks::block_row<foreign_residual, blocks::density::mass::value>;
using unknown_residual_jacobian = blocks::type_list<
gradient_row, poisson_row, density_row, displacement_row, enthalpy_row,
unknown_residual_row>;
using duplicate_value_form = blocks::block_form<
blocks::type_list<
blocks::density::mass::value, blocks::density::mass::value>,
blocks::type_list<blocks::density::mass::residual>>;
STATIC_REQUIRE(blocks::block_form_is_valid_v<form>);
STATIC_REQUIRE((blocks::valid_jacobian_form<form, jacobian>));
STATIC_REQUIRE_FALSE(blocks::block_form_is_valid_v<duplicate_value_form>);
STATIC_REQUIRE_FALSE((blocks::valid_jacobian_form<form, missing_row>));
STATIC_REQUIRE_FALSE((blocks::valid_jacobian_form<form, duplicate_row>));
STATIC_REQUIRE_FALSE((blocks::valid_jacobian_form<form, reordered_rows>));
STATIC_REQUIRE_FALSE((
blocks::valid_jacobian_form<form, foreign_value_jacobian>
));
STATIC_REQUIRE_FALSE((
blocks::valid_jacobian_form<form, duplicate_value_jacobian>
));
STATIC_REQUIRE_FALSE((
blocks::valid_jacobian_form<form, unknown_residual_jacobian>
));
CHECK(true);
}
TEST_CASE(
"Barotropic Layout Preserves Distinct Row And Column Offsets",
tags::unit &tags::solver &tags::utils
) {
using form = blocks::barotropic_equilibrium_form;
// Columns: [rho, d, g, Phi, h, C]
const std::array<int, form::value_block_count> value_sizes{11, 13, 17,
19, 23, 1};
// Rows: [R_g, R_Phi, R_rho, R_d, R_h, R_M]
const std::array<int, form::residual_block_count> residual_sizes{17, 19, 11,
13, 23, 1};
const blocks::form_layout<form> layout(value_sizes, residual_sizes);
constexpr auto constant_value = blocks::get_value_block<form>(
blocks::barotropic_constant_field.mass_normalization_term
);
constexpr auto mass_residual = blocks::get_residual_block<form>(
blocks::barotropic_constant_field.mass_normalization_term
);
CHECK(layout.size(constant_value) == 1);
CHECK(layout.size(mass_residual) == 1);
CHECK(layout.offset(constant_value) == 83);
CHECK(layout.offset(mass_residual) == 83);
CHECK(layout.value_offsets().Last() == 84);
CHECK(layout.residual_offsets().Last() == 84);
const std::array<int, form::value_block_count> invalid_value_sizes{11, 13,
17, 19,
23, 2};
CHECK_THROWS(
blocks::form_layout<form>(invalid_value_sizes, residual_sizes)
);
}