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

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

View File

@@ -1,297 +1,416 @@
#include <cstdint>
#include <catch2/catch_test_macros.hpp>
#include <cmath>
#include <cstdint>
#include <mfem.hpp>
#include <type_traits>
import mean_field;
import test_helpers;
namespace barotropic_closure_context_test_utils {
mfem::Vector project_field(
mfem::ParFiniteElementSpace &finiteElementSpace,
mfem::Coefficient &coefficient
namespace field = mean_field::field;
namespace domain = mean_field::utils::domain;
namespace context = mean_field::operators::context::barotropic;
using Schema = domain::CoreEnvelopeVacuumDomainSchema;
struct Maps final {
field::FieldDofMap density;
field::FieldDofMap enthalpy;
field::FieldDofMap displacement;
explicit Maps(const mean_field::fem::FEM &f)
: density(
field::make_field_dof_map<
field::Density,
Schema>(*f.densityFes)
),
enthalpy(
field::make_field_dof_map<
field::Enthalpy,
Schema>(*f.enthalpyFes)
),
displacement(
field::make_field_dof_map<
field::Displacement,
Schema>(*f.displacementFes)
) {
}
};
[[nodiscard]] context::BarotropicClosureDependencies make_dependencies() {
return {
.discretization = {.identity = 101, .revision = 2},
.density = {.identity = 103, .revision = 3},
.enthalpy = {.identity = 107, .revision = 5},
.displacement = {.identity = 109, .revision = 7}
};
}
[[nodiscard]] context::BarotropicClosureStateView make_state(
const mfem::Vector &density,
const mfem::Vector &enthalpy,
const mfem::Vector &displacement
) {
mfem::ParGridFunction field(&finiteElementSpace);
field.ProjectCoefficient(coefficient);
mfem::Vector trueVector;
field.GetTrueDofs(trueVector);
return trueVector;
return {.density = density, .enthalpy = enthalpy, .displacement = displacement};
}
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);
[[nodiscard]] mfem::Vector reduce(
const field::FieldDofMap &map,
const mfem::Vector &full
) {
return map.gather(full);
}
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);
[[nodiscard]] mfem::Vector make_density(
const mean_field::fem::FEM &f,
const double phase
) {
mfem::ParGridFunction value(f.densityFes.get());
mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) {
return 0.71 + 0.05 * std::sin(0.63 * position(0) + phase) + 0.02 * position(1);
});
value.ProjectCoefficient(coefficient);
mfem::Vector result;
value.GetTrueDofs(result);
return result;
}
return project_field(*f.enthalpyFes, coefficient);
[[nodiscard]] mfem::Vector make_enthalpy(
const mean_field::fem::FEM &f,
const double phase
) {
mfem::ParGridFunction value(f.enthalpyFes.get());
mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) {
return 0.93 + 0.04 * std::cos(0.57 * position(1) - phase) + 0.015 * position(2);
});
value.ProjectCoefficient(coefficient);
mfem::Vector result;
value.GetTrueDofs(result);
return result;
}
[[nodiscard]] double relative_error(
const mfem::Vector &left,
const mfem::Vector &right,
const MPI_Comm communicator
) {
return gravity_prepared_test_utils::relative_error(left, right, communicator);
}
} // namespace barotropic_closure_context_test_utils
TEST_CASE(
"Barotropic Closure Context Tracks Independent Revisions",
tags::barotrope &tags::closure &tags::hydro &tags::prepared &tags::unit
"Prepared Barotropic Closure Owns Its Linearization Context",
tags::barotrope &tags::closure &tags::contexts &tags::prepared &tags::field &tags::unit
) {
auto args = test_utils::setup_args();
using Operator = mean_field::operators::PreparedBarotropicClosureOperator;
using Context = mean_field::operators::context::barotropic::BarotropicClosureLinearizationContext;
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
STATIC_REQUIRE_FALSE(std::is_copy_constructible_v<Context>);
STATIC_REQUIRE_FALSE(std::is_copy_assignable_v<Context>);
STATIC_REQUIRE_FALSE(std::is_move_constructible_v<Context>);
STATIC_REQUIRE_FALSE(std::is_move_assignable_v<Context>);
const mean_field::physics::PolytropicBarotrope barotrope(3.0, 1.5);
auto args = test_utils::setup_args();
mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.okay());
mean_field::operators::context::barotropic::
BarotropicClosureLinearizationContext context(
f, *f.domainMapperStateless, barotrope
);
const mean_field::eos::Polytrope equationOfState(3.0, 1.5);
Operator preparedOperator(f, *f.domainMapperStateless, equationOfState);
mfem::Vector density =
barotropic_closure_context_test_utils::make_density(f);
CHECK_FALSE(preparedOperator.IsPrepared());
CHECK_FALSE(preparedOperator.GetContext().IsPrepared());
CHECK(&preparedOperator.GetContext() == &preparedOperator.GetContext());
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);
const auto statistics = preparedOperator.GetContextPreparationStatistics();
CHECK(statistics.staticPreparations == 0);
CHECK(statistics.geometryPreparations == 0);
CHECK(statistics.baseStatePreparations == 0);
}
TEST_CASE(
"Barotropic Closure Context Reprepares A Consistent Frozen State",
tags::barotrope &tags::closure &tags::hydro &tags::prepared &tags::unit
"Barotropic Closure Context Applies Selective Invalidation In Reduced Field Coordinates",
tags::barotrope &tags::closure &tags::contexts &tags::prepared &tags::field &tags::unit
) {
auto args = test_utils::setup_args();
using namespace barotropic_closure_context_test_utils;
mean_field::fem::FEM f =
mean_field::fem::setup_fem(args.mesh_file, args, 0);
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);
REQUIRE(f.okay());
mean_field::operators::context::barotropic::
BarotropicClosureLinearizationContext context(
f, *f.domainMapperStateless, barotrope
);
const Maps maps(f);
const mfem::Vector density =
barotropic_closure_context_test_utils::make_density(f);
const mean_field::eos::Polytrope equationOfState(3.0, 1.5);
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
mean_field::operators::PreparedBarotropicClosureOperator preparedOperator(
f, *f.domainMapperStateless, equationOfState
);
mfem::Vector changedDensity(density);
changedDensity.Add(0.025, densityVariation);
mfem::Vector density = reduce(maps.density, make_density(f, 0.17));
mfem::Vector changedEnthalpy(enthalpy);
changedEnthalpy.Add(0.015, enthalpyVariation);
mfem::Vector enthalpy = reduce(maps.enthalpy, make_enthalpy(f, 0.29));
const mfem::Vector deformedDisplacement =
gravity_prepared_test_utils::make_displacement(f, 1.0);
const mfem::Vector initialDisplacement =
reduce(maps.displacement, gravity_prepared_test_utils::make_displacement(f, 0.35));
context.Prepare(
changedDensity, changedEnthalpy, deformedDisplacement, revisions
);
/*
* A second smooth, orientation-preserving geometry.
*
* Do not manufacture a new geometry by perturbing an arbitrary H1
* coefficient. A modest change in one high-order displacement DOF can
* correspond to a very large local displacement gradient and can invert
* an element.
*/
const mfem::Vector changedDisplacement =
reduce(maps.displacement, gravity_prepared_test_utils::make_displacement(f, 0.85));
mfem::Vector unchangedResidual;
mfem::Vector unchangedAction;
mfem::Vector displacement(initialDisplacement);
context.BuildResidual(unchangedResidual);
auto dependencies = make_dependencies();
context.GetOperator().Mult(
densityVariation, enthalpyVariation, displacementVariation,
unchangedAction
);
const auto initialReport = preparedOperator.Prepare(make_state(density, enthalpy, displacement), dependencies);
const auto &context = preparedOperator.GetContext();
REQUIRE(preparedOperator.IsPrepared());
REQUIRE(context.IsPrepared());
CHECK(context.MatchesDependencies(dependencies));
CHECK(context.GetDependencies() == dependencies);
CHECK(initialReport.contextReport.preparedStaticDependencies);
CHECK(initialReport.contextReport.preparedGeometryState);
CHECK(initialReport.contextReport.preparedBaseState);
CHECK(initialReport.contextReport.updatedDensity);
CHECK(initialReport.contextReport.updatedEnthalpy);
CHECK(initialReport.contextReport.updatedDisplacement);
CHECK(initialReport.preparedElementData);
CHECK(initialReport.DidAnyWork());
CHECK(preparedOperator.GetPreparationCount() == 1);
const mfem::Vector frozenDensity = context.GetBaseDensity();
const mfem::Vector frozenEnthalpy = context.GetBaseEnthalpy();
const mfem::Vector frozenDisplacement = context.GetDisplacement();
/*
* Modify all three candidate states without updating their dependency
* stamps.
*
* The context must continue exposing the previously frozen state.
*/
density(0) += 0.25;
enthalpy(0) -= 0.18;
displacement = changedDisplacement;
const auto repeatedReport = preparedOperator.Prepare(make_state(density, enthalpy, displacement), dependencies);
CHECK_FALSE(repeatedReport.DidAnyWork());
CHECK_FALSE(repeatedReport.contextReport.updatedDensity);
CHECK_FALSE(repeatedReport.contextReport.updatedEnthalpy);
CHECK_FALSE(repeatedReport.contextReport.updatedDisplacement);
CHECK_FALSE(repeatedReport.preparedElementData);
CHECK(preparedOperator.GetPreparationCount() == 1);
const MPI_Comm communicator = f.mesh->GetComm();
CHECK(context.GetPreparationCount() == 1);
CHECK(relative_error(context.GetBaseDensity(), frozenDensity, communicator) == 0.0);
CHECK(
gravity_prepared_test_utils::relative_error(
unchangedResidual, initialResidual, communicator
) < 5.0e-15
CHECK(relative_error(context.GetBaseEnthalpy(), frozenEnthalpy, communicator) == 0.0);
CHECK(relative_error(context.GetDisplacement(), frozenDisplacement, communicator) == 0.0);
/*
* Density invalidation.
*
* Geometry remains frozen because the displacement dependency did not
* change.
*/
++dependencies.density.revision;
const auto densityReport = preparedOperator.Prepare(make_state(density, enthalpy, displacement), dependencies);
CHECK_FALSE(densityReport.contextReport.preparedStaticDependencies);
CHECK_FALSE(densityReport.contextReport.preparedGeometryState);
CHECK(densityReport.contextReport.preparedBaseState);
CHECK(densityReport.contextReport.updatedDensity);
CHECK_FALSE(densityReport.contextReport.updatedEnthalpy);
CHECK_FALSE(densityReport.contextReport.updatedDisplacement);
CHECK(densityReport.preparedElementData);
CHECK(context.GetBaseDensity()(0) == density(0));
/*
* The candidate displacement has changed, but because its revision has
* not changed the context must still retain the original geometry.
*/
CHECK(relative_error(context.GetDisplacement(), frozenDisplacement, communicator) == 0.0);
/*
* Enthalpy invalidation.
*/
++dependencies.enthalpy.revision;
const auto enthalpyReport = preparedOperator.Prepare(make_state(density, enthalpy, displacement), dependencies);
CHECK_FALSE(enthalpyReport.contextReport.preparedStaticDependencies);
CHECK_FALSE(enthalpyReport.contextReport.preparedGeometryState);
CHECK(enthalpyReport.contextReport.preparedBaseState);
CHECK_FALSE(enthalpyReport.contextReport.updatedDensity);
CHECK(enthalpyReport.contextReport.updatedEnthalpy);
CHECK_FALSE(enthalpyReport.contextReport.updatedDisplacement);
CHECK(enthalpyReport.preparedElementData);
CHECK(context.GetBaseEnthalpy()(0) == enthalpy(0));
CHECK(relative_error(context.GetDisplacement(), frozenDisplacement, communicator) == 0.0);
/*
* Displacement invalidation.
*
* The changed geometry is now intentionally accepted. Because it came
* from the smooth test displacement projection rather than an arbitrary
* single H1 coefficient mutation, it remains a valid mapping.
*/
++dependencies.displacement.revision;
const auto displacementReport = preparedOperator.Prepare(make_state(density, enthalpy, displacement), dependencies);
CHECK_FALSE(displacementReport.contextReport.preparedStaticDependencies);
CHECK(displacementReport.contextReport.preparedGeometryState);
CHECK(displacementReport.contextReport.preparedBaseState);
CHECK_FALSE(displacementReport.contextReport.updatedDensity);
CHECK_FALSE(displacementReport.contextReport.updatedEnthalpy);
CHECK(displacementReport.contextReport.updatedDisplacement);
CHECK(displacementReport.preparedElementData);
CHECK(relative_error(context.GetDisplacement(), changedDisplacement, communicator) == 0.0);
/*
* Discretization invalidates everything.
*/
++dependencies.discretization.revision;
const auto discretizationReport =
preparedOperator.Prepare(make_state(density, enthalpy, displacement), dependencies);
CHECK(discretizationReport.contextReport.preparedStaticDependencies);
CHECK(discretizationReport.contextReport.preparedGeometryState);
CHECK(discretizationReport.contextReport.preparedBaseState);
CHECK(discretizationReport.contextReport.updatedDensity);
CHECK(discretizationReport.contextReport.updatedEnthalpy);
CHECK(discretizationReport.contextReport.updatedDisplacement);
CHECK(discretizationReport.preparedElementData);
const auto finalStatistics = preparedOperator.GetContextPreparationStatistics();
CHECK(finalStatistics.staticPreparations == 2);
CHECK(finalStatistics.geometryPreparations == 3);
CHECK(finalStatistics.baseStatePreparations == 5);
CHECK(preparedOperator.GetPreparationCount() == 5);
}
TEST_CASE(
"Barotropic Closure Context Uses Identity And Revision For Every Dependency",
tags::barotrope &tags::closure &tags::contexts &tags::prepared &tags::field &tags::unit
) {
using namespace barotropic_closure_context_test_utils;
auto args = test_utils::setup_args();
mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.okay());
const Maps maps(f);
const mean_field::eos::Polytrope equationOfState(3.0, 1.5);
mean_field::operators::PreparedBarotropicClosureOperator preparedOperator(
f, *f.domainMapperStateless, equationOfState
);
CHECK(
gravity_prepared_test_utils::relative_error(
unchangedAction, initialAction, communicator
) < 5.0e-15
);
mfem::Vector density = reduce(maps.density, make_density(f, 0.41));
mfem::Vector enthalpy = reduce(maps.enthalpy, make_enthalpy(f, 0.53));
mfem::Vector displacement = reduce(maps.displacement, gravity_prepared_test_utils::make_displacement(f, 0.60));
++revisions.density;
++revisions.enthalpy;
++revisions.displacement;
auto dependencies = make_dependencies();
preparedOperator.Prepare(make_state(density, enthalpy, displacement), dependencies);
context.Prepare(
changedDensity, changedEnthalpy, deformedDisplacement, revisions
);
const mfem::Vector frozenDensity = preparedOperator.GetContext().GetBaseDensity();
density(0) += 0.19;
mfem::Vector preparedResidual;
mfem::Vector preparedAction;
const auto sameStampReport = preparedOperator.Prepare(make_state(density, enthalpy, displacement), dependencies);
CHECK_FALSE(sameStampReport.DidAnyWork());
CHECK(preparedOperator.GetContext().GetBaseDensity()(0) == frozenDensity(0));
context.BuildResidual(preparedResidual);
++dependencies.density.identity;
++dependencies.density.revision;
context.GetOperator().Mult(
densityVariation, enthalpyVariation, displacementVariation,
preparedAction
);
const auto newIdentityReport = preparedOperator.Prepare(make_state(density, enthalpy, displacement), dependencies);
CHECK_FALSE(newIdentityReport.contextReport.preparedStaticDependencies);
CHECK_FALSE(newIdentityReport.contextReport.preparedGeometryState);
CHECK(newIdentityReport.contextReport.preparedBaseState);
CHECK(newIdentityReport.contextReport.updatedDensity);
CHECK(preparedOperator.GetContext().GetBaseDensity()(0) == density(0));
CHECK(preparedOperator.GetContext().MatchesDependencies(dependencies));
mfem::Vector referenceResidual;
mfem::Vector referenceDensityAction;
mfem::Vector referenceEnthalpyAction;
mfem::Vector referenceDisplacementAction;
++dependencies.displacement.identity;
++dependencies.displacement.revision;
mean_field::operators::kernels::apply_barotropic_closure(
f, *f.domainMapperStateless, barotrope, changedDensity, changedEnthalpy,
deformedDisplacement, referenceResidual
);
const auto displacementIdentityReport =
preparedOperator.Prepare(make_state(density, enthalpy, displacement), dependencies);
CHECK_FALSE(displacementIdentityReport.contextReport.preparedStaticDependencies);
CHECK(displacementIdentityReport.contextReport.preparedGeometryState);
CHECK(displacementIdentityReport.contextReport.preparedBaseState);
CHECK(displacementIdentityReport.contextReport.updatedDisplacement);
mean_field::operators::kernels::apply_barotropic_closure_density_action(
f, *f.domainMapperStateless, barotrope, densityVariation,
deformedDisplacement, referenceDensityAction
);
++dependencies.discretization.identity;
++dependencies.discretization.revision;
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);
}
const auto discretizationIdentityReport =
preparedOperator.Prepare(make_state(density, enthalpy, displacement), dependencies);
CHECK(discretizationIdentityReport.contextReport.preparedStaticDependencies);
CHECK(discretizationIdentityReport.contextReport.preparedGeometryState);
CHECK(discretizationIdentityReport.contextReport.preparedBaseState);
}