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

@@ -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);
}