feat(newton): first newton solver implementation

This commit is contained in:
2026-09-08 06:36:39 -04:00
parent 76818f2f82
commit b3c04d507a
98 changed files with 20397 additions and 11040 deletions

View File

@@ -9,6 +9,28 @@ using namespace mean_field;
namespace prepared_test = gravity_prepared_test_utils;
namespace gravity_context = operators::context::gravity_field;
namespace {
[[nodiscard]] mfem::Vector makeAffineDisplacement(
const fem::FEM &finiteElements,
const double scale
) {
mfem::ParGridFunction field(finiteElements.displacementFes.get());
mfem::VectorFunctionCoefficient coefficient(
finiteElements.mesh->Dimension(), [scale](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(position.Size());
for (int component = 0; component < position.Size(); ++component) {
value(component) = scale * position(component);
}
}
);
field.ProjectCoefficient(coefficient);
mfem::Vector result;
field.GetTrueDofs(result);
return result;
}
} // namespace
TEST_CASE(
"Gravity Field Linearization Context Applies Selective Invalidation",
tags::gravity_context
@@ -125,6 +147,70 @@ TEST_CASE(
CHECK(context.GetGeometryContext().GetSourceOperator().GetPreparationCount() == 1);
}
TEST_CASE(
"Gravity Field Contexts Invalidate And Recover After Candidate Rejection",
tags::gravity_context &tags::geometry &tags::unit
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.okay());
gravity_context::GravityFieldGeometryContext geometryContext(f, *f.domainMapperStateless);
const mfem::Vector zeroDisplacement = geometryContext.GetDisplacementMap().gather(makeAffineDisplacement(f, 0.0));
const mfem::Vector foldedDisplacement =
geometryContext.GetDisplacementMap().gather(makeAffineDisplacement(f, -2.0));
REQUIRE(geometryContext.TryPrepare(zeroDisplacement, {.value = 0}, {.value = 0}).has_value());
REQUIRE(geometryContext.IsPrepared());
const auto geometryRejection = geometryContext.TryPrepare(foldedDisplacement, {.value = 0}, {.value = 1});
REQUIRE_FALSE(geometryRejection.has_value());
CHECK(geometryRejection.error().reason == gravity_context::GravityFieldPreparationRejectionReason::invalid_mapping);
CHECK(geometryRejection.error().mappingStatus == mapping::MappingStatus::non_positive_determinant);
CHECK_FALSE(geometryContext.IsPrepared());
const auto geometryRecovery = geometryContext.TryPrepare(zeroDisplacement, {.value = 0}, {.value = 1});
REQUIRE(geometryRecovery.has_value());
CHECK(geometryRecovery->reconstructed_operators);
CHECK(geometryContext.IsPrepared());
gravity_context::GravityFieldLinearizationContext linearizationContext(f, *f.domainMapperStateless);
mfem::Vector density =
prepared_test::make_deterministic_vector(linearizationContext.GetDensityMap().reduced_size(), 0.17);
mfem::Vector gravityGradient =
prepared_test::make_deterministic_vector(linearizationContext.GetGravityGradientMap().reduced_size(), 0.41);
mfem::Vector gravityPotential =
prepared_test::make_deterministic_vector(linearizationContext.GetGravityPotentialMap().reduced_size(), 0.73);
gravity_context::GravityFieldRevisions revisions;
const auto makeState = [&](const mfem::Vector &displacement) {
return gravity_context::GravityFieldStateView{
.density = density,
.displacement = displacement,
.gravity_gradient = gravityGradient,
.gravity_potential = gravityPotential
};
};
REQUIRE(linearizationContext.TryPrepare(makeState(zeroDisplacement), revisions).has_value());
REQUIRE(linearizationContext.IsPrepared());
revisions.displacement.value = 1;
const auto linearizationRejection = linearizationContext.TryPrepare(makeState(foldedDisplacement), revisions);
REQUIRE_FALSE(linearizationRejection.has_value());
CHECK(
linearizationRejection.error().reason ==
gravity_context::GravityFieldPreparationRejectionReason::invalid_mapping
);
CHECK(linearizationRejection.error().mappingStatus == mapping::MappingStatus::non_positive_determinant);
CHECK_FALSE(linearizationContext.IsPrepared());
const auto linearizationRecovery = linearizationContext.TryPrepare(makeState(zeroDisplacement), revisions);
REQUIRE(linearizationRecovery.has_value());
CHECK(linearizationRecovery->geometry.reconstructed_operators);
CHECK(linearizationContext.IsPrepared());
}
TEST_CASE(
"Gravity Field Geometry Context Distinguishes Primal And Linearization Preparation",
tags::gravity_context

View File

@@ -2,6 +2,7 @@
#include <array>
#include <cmath>
#include <limits>
#include <stdexcept>
#include <catch2/catch_test_macros.hpp>
@@ -177,6 +178,26 @@ namespace gravity_displacement_force_test_utils {
return direction;
}
[[nodiscard]] mfem::Vector make_affine_displacement(
const mean_field::fem::FEM &f,
const double scale
) {
mfem::ParGridFunction field(f.displacementFes.get());
mfem::VectorFunctionCoefficient coefficient(
f.mesh->Dimension(), [scale](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(position.Size());
for (int dimension = 0; dimension < position.Size(); ++dimension) {
value(dimension) = scale * position(dimension);
}
}
);
field.ProjectCoefficient(coefficient);
mfem::Vector result;
field.GetTrueDofs(result);
return result;
}
[[nodiscard]] mfem::Vector make_vacuum_only_density(const mean_field::fem::FEM &f) {
mfem::ParGridFunction densityField(f.densityFes.get());
densityField = 0.0;
@@ -422,6 +443,85 @@ TEST_CASE(
CHECK(gravity_prepared_test_utils::global_norm(vacuumResidual, f.mesh->GetComm()) == 0.0);
}
TEST_CASE(
"Gravity Displacement Force Reports Candidate Mapping And Arithmetic Rejections",
tags::gravity_unit
) {
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());
mfem::Vector density = gravity_displacement_force_test_utils::make_density(f, 0.21);
mfem::Vector gravityGradient = gravity_displacement_force_test_utils::make_gravity_gradient(f, 0.37);
mfem::Vector displacement = gravity_displacement_force_test_utils::make_affine_displacement(f, -2.0);
mfem::Vector residual;
const auto invalidMapping = mean_field::operators::kernels::try_apply_gravity_displacement_force_residual(
f, *f.domainMapperStateless, density, gravityGradient, displacement, residual
);
REQUIRE_FALSE(invalidMapping.has_value());
CHECK(
invalidMapping.error().reason ==
mean_field::operators::kernels::GravityDisplacementForceRejectionReason::invalid_mapping
);
CHECK(invalidMapping.error().mappingStatus == mean_field::mapping::MappingStatus::non_positive_determinant);
CHECK_THROWS_AS(
mean_field::operators::kernels::apply_gravity_displacement_force_residual(
f, *f.domainMapperStateless, density, gravityGradient, displacement, residual
),
std::domain_error
);
displacement = 0.0;
density = 1.0e200;
gravityGradient = 1.0e200;
const auto nonFiniteArithmetic = mean_field::operators::kernels::try_apply_gravity_displacement_force_residual(
f, *f.domainMapperStateless, density, gravityGradient, displacement, residual
);
REQUIRE_FALSE(nonFiniteArithmetic.has_value());
CHECK(
nonFiniteArithmetic.error().reason ==
mean_field::operators::kernels::GravityDisplacementForceRejectionReason::non_finite_arithmetic
);
mfem::Vector gravityPotential(f.gravityPotentialFes->GetTrueVSize());
gravityPotential = 0.0;
auto revisions = gravity_displacement_force_test_utils::make_revisions();
mean_field::operators::context::gravity_field::GravityFieldLinearizationContext gravityContext(
f, *f.domainMapperStateless
);
gravity_displacement_force_test_utils::prepare_gravity_context(
gravityContext, density, displacement, gravityGradient, gravityPotential, revisions
);
mean_field::operators::PreparedGravityDisplacementForceOperator preparedOperator(
f, *f.domainMapperStateless, gravityContext
);
const auto preparedRejection = preparedOperator.TryPrepare();
REQUIRE_FALSE(preparedRejection.has_value());
CHECK(
preparedRejection.error().reason ==
mean_field::operators::kernels::GravityDisplacementForceRejectionReason::non_finite_arithmetic
);
CHECK_FALSE(preparedOperator.IsPrepared());
CHECK_THROWS_AS(preparedOperator.Prepare(), std::domain_error);
density = gravity_displacement_force_test_utils::make_density(f, 0.21);
gravityGradient = gravity_displacement_force_test_utils::make_gravity_gradient(f, 0.37);
++revisions.density.value;
++revisions.gravity_gradient.value;
gravity_displacement_force_test_utils::prepare_gravity_context(
gravityContext, density, displacement, gravityGradient, gravityPotential, revisions
);
const auto preparedAccepted = preparedOperator.TryPrepare();
REQUIRE(preparedAccepted.has_value());
CHECK(preparedOperator.IsPrepared());
const auto accepted = mean_field::operators::kernels::try_apply_gravity_displacement_force_residual(
f, *f.domainMapperStateless, density, gravityGradient, displacement, residual
);
CHECK(accepted.has_value());
}
TEST_CASE(
"Prepared Gravity Displacement Force Reuses Shared Gravity Revisions",
tags::gravity_prepared

View File

@@ -3,6 +3,7 @@
#include <cmath>
#include <cstdint>
#include <limits>
#include <stdexcept>
#include <type_traits>
#include <catch2/catch_approx.hpp>
@@ -16,22 +17,22 @@ namespace angular_momentum_test_utils {
[[nodiscard]] mean_field::operators::AngularMomentumDependencies makeDependencies() {
return {
.discretization = {.identity = 15013, .revision = 3},
.density = {.identity = 15017, .revision = 5},
.displacement = {.identity = 15031, .revision = 7},
.rotation = {.identity = 15053, .revision = 11}
.density = {.identity = 15017, .revision = 5},
.displacement = {.identity = 15031, .revision = 7},
.rotation = {.identity = 15053, .revision = 11}
};
}
[[nodiscard]] mean_field::operators::context::gravity_field::GravityFieldRevisions makeGravityRevisions(
const mean_field::operators::AngularMomentumDependencies &dependencies,
const std::uint64_t gravityGradientRevision = 13,
const std::uint64_t gravityGradientRevision = 13,
const std::uint64_t gravityPotentialRevision = 17
) {
return {
.discretization = {.value = dependencies.discretization.revision},
.displacement = {.value = dependencies.displacement.revision},
.density = {.value = dependencies.density.revision},
.gravity_gradient = {.value = gravityGradientRevision},
.discretization = {.value = dependencies.discretization.revision},
.displacement = {.value = dependencies.displacement.revision},
.density = {.value = dependencies.density.revision},
.gravity_gradient = {.value = gravityGradientRevision},
.gravity_potential = {.value = gravityPotentialRevision}
};
}
@@ -42,17 +43,17 @@ namespace angular_momentum_test_utils {
const mfem::Vector &density,
const mfem::Vector &displacement,
const mean_field::operators::AngularMomentumDependencies &dependencies,
const std::uint64_t gravityGradientRevision = 13,
const std::uint64_t gravityGradientRevision = 13,
const std::uint64_t gravityPotentialRevision = 17
) {
mfem::Vector gravityGradient(finiteElements.gravityFluxFes->GetTrueVSize());
mfem::Vector gravityPotential(finiteElements.gravityPotentialFes->GetTrueVSize());
gravityGradient = 0.0;
gravityGradient = 0.0;
gravityPotential = 0.0;
context.Prepare(
{.density = context.GetDensityMap().gather(density),
.displacement = context.GetDisplacementMap().gather(displacement),
.gravity_gradient = context.GetGravityGradientMap().gather(gravityGradient),
{.density = context.GetDensityMap().gather(density),
.displacement = context.GetDisplacementMap().gather(displacement),
.gravity_gradient = context.GetGravityGradientMap().gather(gravityGradient),
.gravity_potential = context.GetGravityPotentialMap().gather(gravityPotential)},
makeGravityRevisions(dependencies, gravityGradientRevision, gravityPotentialRevision)
);
@@ -64,8 +65,8 @@ namespace angular_momentum_test_utils {
) {
mfem::ParGridFunction field(finiteElements.densityFes.get());
mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) {
return 0.94 + 0.08 * std::sin(0.71 * position(0) + phase) +
0.05 * std::cos(0.63 * position(1) - phase) + 0.03 * position(2) * position(2);
return 0.94 + 0.08 * std::sin(0.71 * position(0) + phase) + 0.05 * std::cos(0.63 * position(1) - phase) +
0.03 * position(2) * position(2);
});
field.ProjectCoefficient(coefficient);
mfem::Vector result;
@@ -79,8 +80,8 @@ namespace angular_momentum_test_utils {
) {
mfem::ParGridFunction field(finiteElements.densityFes.get());
mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) {
return 0.17 * std::sin(0.83 * position(0) + phase) -
0.12 * std::cos(0.79 * position(1) - phase) + 0.06 * position(2);
return 0.17 * std::sin(0.83 * position(0) + phase) - 0.12 * std::cos(0.79 * position(1) - phase) +
0.06 * position(2);
});
field.ProjectCoefficient(coefficient);
mfem::Vector result;
@@ -110,8 +111,7 @@ namespace angular_momentum_test_utils {
) {
mfem::ParGridFunction field(finiteElements.displacementFes.get());
mfem::VectorFunctionCoefficient coefficient(
finiteElements.mesh->Dimension(),
[scale](const mfem::Vector &position, mfem::Vector &value) {
finiteElements.mesh->Dimension(), [scale](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(position.Size());
for (int component = 0; component < position.Size(); ++component) {
value(component) = scale * position(component);
@@ -130,8 +130,7 @@ namespace angular_momentum_test_utils {
) {
mfem::ParGridFunction field(finiteElements.displacementFes.get());
mfem::VectorFunctionCoefficient coefficient(
finiteElements.mesh->Dimension(),
[scale](const mfem::Vector &position, mfem::Vector &value) {
finiteElements.mesh->Dimension(), [scale](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(3);
value(0) = scale * (0.07 * position(0) + 0.018 * position(1) * position(2));
value(1) = scale * (-0.05 * position(1) + 0.013 * position(0) * position(2));
@@ -151,7 +150,10 @@ namespace angular_momentum_test_utils {
return value(0);
}
[[nodiscard]] double relativeError(const double actual, const double expected) {
[[nodiscard]] double relativeError(
const double actual,
const double expected
) {
return std::abs(actual - expected) /
std::max({std::abs(actual), std::abs(expected), 100.0 * std::numeric_limits<double>::epsilon()});
}
@@ -168,48 +170,34 @@ TEST_CASE(
STATIC_CHECK_FALSE(std::is_copy_constructible_v<Operator>);
STATIC_CHECK_FALSE(std::is_move_constructible_v<Operator>);
utils::Args arguments = test_utils::setup_args();
utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
constexpr double densityValue = 1.37;
constexpr double angularVelocity = 0.73;
constexpr double densityValue = 1.37;
constexpr double angularVelocity = 0.73;
constexpr double targetAngularMomentum = 0.41;
mfem::ParGridFunction densityField(finiteElements.densityFes.get());
const mfem::Vector density = angular_momentum_test_utils::projectConstantDensity(
finiteElements,
densityValue,
&densityField
);
const mfem::Vector density =
angular_momentum_test_utils::projectConstantDensity(finiteElements, densityValue, &densityField);
mfem::Vector displacement(finiteElements.displacementFes->GetTrueVSize());
displacement = 0.0;
finiteElements.displacement->SetFromTrueDofs(displacement);
auto dependencies = angular_momentum_test_utils::makeDependencies();
operators::context::gravity_field::GravityFieldLinearizationContext gravityContext(
finiteElements,
*finiteElements.domainMapperStateless
finiteElements, *finiteElements.domainMapperStateless
);
angular_momentum_test_utils::prepareGravityContext(
gravityContext,
finiteElements,
density,
displacement,
dependencies
gravityContext, finiteElements, density, displacement, dependencies
);
const models::CompiledFixedAngularMomentum originConstraint = models::compileConstraint(
integral::FixedAngularMomentum({
.Jtotal = dimensions::AngularMomentumValue{targetAngularMomentum},
.axis = {0.0, 0.0, 4.0}
})
);
Operator origin(
finiteElements,
*finiteElements.domainMapperStateless,
gravityContext,
originConstraint
integral::FixedAngularMomentum(
{.Jtotal = dimensions::AngularMomentumValue{targetAngularMomentum}, .axis = {0.0, 0.0, 4.0}}
)
);
Operator origin(finiteElements, *finiteElements.domainMapperStateless, gravityContext, originConstraint);
const auto initial = origin.Prepare(angularVelocity, dependencies);
CHECK(initial.rebuiltStaticPlan);
CHECK(initial.refreshedGeometry);
@@ -219,10 +207,11 @@ TEST_CASE(
const double independentMoment = analysis::get_moment_of_inertia(finiteElements, densityField);
CHECK(angular_momentum_test_utils::relativeError(origin.GetMomentOfInertia(), independentMoment) < 2.0e-13);
CHECK(origin.GetCurrentAngularMomentum() ==
Approx(angularVelocity * origin.GetMomentOfInertia()).epsilon(2.0e-15));
CHECK(angular_momentum_test_utils::residual(origin) ==
Approx(angularVelocity * origin.GetMomentOfInertia() - targetAngularMomentum).epsilon(2.0e-15));
CHECK(origin.GetCurrentAngularMomentum() == Approx(angularVelocity * origin.GetMomentOfInertia()).epsilon(2.0e-15));
CHECK(
angular_momentum_test_utils::residual(origin) ==
Approx(angularVelocity * origin.GetMomentOfInertia() - targetAngularMomentum).epsilon(2.0e-15)
);
const auto report = origin.GetConstraintReport();
CHECK(report.targetAngularMomentum == targetAngularMomentum);
@@ -239,11 +228,7 @@ TEST_CASE(
angular_momentum_test_utils::projectAffineDisplacement(finiteElements, affineScale);
++dependencies.displacement.revision;
angular_momentum_test_utils::prepareGravityContext(
gravityContext,
finiteElements,
density,
affineDisplacement,
dependencies
gravityContext, finiteElements, density, affineDisplacement, dependencies
);
const auto affine = origin.Prepare(angularVelocity, dependencies);
CHECK(affine.refreshedGeometry);
@@ -257,38 +242,30 @@ TEST_CASE(
displacement = 0.0;
++dependencies.displacement.revision;
angular_momentum_test_utils::prepareGravityContext(
gravityContext,
finiteElements,
density,
displacement,
dependencies
gravityContext, finiteElements, density, displacement, dependencies
);
origin.Prepare(angularVelocity, dependencies);
constexpr std::array<double, 3> shiftedCenter{0.27, -0.19, 0.31};
Operator shifted(
finiteElements,
*finiteElements.domainMapperStateless,
gravityContext,
models::compileConstraint(integral::FixedAngularMomentum({
.Jtotal = dimensions::AngularMomentumValue{targetAngularMomentum},
.axis = {0.0, 0.0, 1.0},
.center = shiftedCenter
}))
finiteElements, *finiteElements.domainMapperStateless, gravityContext,
models::compileConstraint(
integral::FixedAngularMomentum(
{.Jtotal = dimensions::AngularMomentumValue{targetAngularMomentum},
.axis = {0.0, 0.0, 1.0},
.center = shiftedCenter}
)
)
);
shifted.Prepare(angularVelocity, dependencies);
const double mass = analysis::domain_integrate_grid_function(
finiteElements,
densityField,
utils::DOMAINS::STELLAR,
mapping::COORDINATE_SPACE::PHYSICAL
finiteElements, densityField, utils::DOMAINS::STELLAR, mapping::COORDINATE_SPACE::PHYSICAL
);
const mfem::Vector centerOfMass = analysis::get_com(finiteElements, densityField);
const double expectedShiftedMoment = origin.GetMomentOfInertia() +
mass * (shiftedCenter[0] * shiftedCenter[0] +
shiftedCenter[1] * shiftedCenter[1]) -
2.0 * mass * (shiftedCenter[0] * centerOfMass(0) +
shiftedCenter[1] * centerOfMass(1));
const double expectedShiftedMoment =
origin.GetMomentOfInertia() +
mass * (shiftedCenter[0] * shiftedCenter[0] + shiftedCenter[1] * shiftedCenter[1]) -
2.0 * mass * (shiftedCenter[0] * centerOfMass(0) + shiftedCenter[1] * centerOfMass(1));
CHECK(angular_momentum_test_utils::relativeError(shifted.GetMomentOfInertia(), expectedShiftedMoment) < 3.0e-13);
}
@@ -298,45 +275,33 @@ TEST_CASE(
) {
using namespace mean_field;
utils::Args arguments = test_utils::setup_args();
utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
const mfem::Vector density = angular_momentum_test_utils::projectDensity(finiteElements, 0.31);
const mfem::Vector densityDirection =
angular_momentum_test_utils::projectDensityDirection(finiteElements, 0.67);
const mfem::Vector displacement =
angular_momentum_test_utils::projectDisplacementDirection(finiteElements, 0.43);
const mfem::Vector density = angular_momentum_test_utils::projectDensity(finiteElements, 0.31);
const mfem::Vector densityDirection = angular_momentum_test_utils::projectDensityDirection(finiteElements, 0.67);
const mfem::Vector displacement = angular_momentum_test_utils::projectDisplacementDirection(finiteElements, 0.43);
const mfem::Vector displacementDirection =
angular_momentum_test_utils::projectDisplacementDirection(finiteElements, -0.79);
constexpr double angularVelocity = 0.63;
constexpr double angularVelocity = 0.63;
constexpr double angularVelocityDirection = -0.37;
auto dependencies = angular_momentum_test_utils::makeDependencies();
auto dependencies = angular_momentum_test_utils::makeDependencies();
operators::context::gravity_field::GravityFieldLinearizationContext gravityContext(
finiteElements,
*finiteElements.domainMapperStateless
finiteElements, *finiteElements.domainMapperStateless
);
angular_momentum_test_utils::prepareGravityContext(
gravityContext,
finiteElements,
density,
displacement,
dependencies
gravityContext, finiteElements, density, displacement, dependencies
);
operators::PreparedAngularMomentumOperator operation(
finiteElements,
*finiteElements.domainMapperStateless,
gravityContext,
models::compileConstraint(
integral::FixedAngularMomentum({.Jtotal = dimensions::AngularMomentumValue{0.81}})
)
finiteElements, *finiteElements.domainMapperStateless, gravityContext,
models::compileConstraint(integral::FixedAngularMomentum({.Jtotal = dimensions::AngularMomentumValue{0.81}}))
);
operation.Prepare(angularVelocity, dependencies);
const mfem::Vector reducedDensityDirection = gravityContext.GetDensityMap().gather(densityDirection);
const mfem::Vector reducedDisplacementDirection =
gravityContext.GetDisplacementMap().gather(displacementDirection);
const mfem::Vector reducedDensityDirection = gravityContext.GetDensityMap().gather(densityDirection);
const mfem::Vector reducedDisplacementDirection = gravityContext.GetDisplacementMap().gather(displacementDirection);
mfem::Vector densityAction;
mfem::Vector geometryAction;
mfem::Vector angularVelocityAction;
@@ -345,17 +310,17 @@ TEST_CASE(
operation.ApplyDisplacementJacobianAction(reducedDisplacementDirection, geometryAction);
operation.ApplyAngularVelocityJacobianAction(angularVelocityDirection, angularVelocityAction);
operation.ApplyCompleteJacobianAction(
reducedDensityDirection,
reducedDisplacementDirection,
angularVelocityDirection,
completeAction
reducedDensityDirection, reducedDisplacementDirection, angularVelocityDirection, completeAction
);
CHECK(
angular_momentum_test_utils::relativeError(
completeAction(0), densityAction(0) + geometryAction(0) + angularVelocityAction(0)
) < 3.0e-15
);
CHECK(
angularVelocityAction(0) ==
Catch::Approx(operation.GetMomentOfInertia() * angularVelocityDirection).epsilon(2.0e-15)
);
CHECK(angular_momentum_test_utils::relativeError(
completeAction(0),
densityAction(0) + geometryAction(0) + angularVelocityAction(0)
) < 3.0e-15);
CHECK(angularVelocityAction(0) ==
Catch::Approx(operation.GetMomentOfInertia() * angularVelocityDirection).epsilon(2.0e-15));
constexpr double angularStep = 1.0e-6;
++dependencies.rotation.revision;
@@ -363,7 +328,7 @@ TEST_CASE(
const double angularPlus = angular_momentum_test_utils::residual(operation);
++dependencies.rotation.revision;
operation.Prepare(angularVelocity - angularStep * angularVelocityDirection, dependencies);
const double angularMinus = angular_momentum_test_utils::residual(operation);
const double angularMinus = angular_momentum_test_utils::residual(operation);
const double angularDifference = (angularPlus - angularMinus) / (2.0 * angularStep);
CHECK(angular_momentum_test_utils::relativeError(angularVelocityAction(0), angularDifference) < 2.0e-10);
@@ -372,11 +337,7 @@ TEST_CASE(
densityPlus.Add(densityStep, densityDirection);
++dependencies.density.revision;
angular_momentum_test_utils::prepareGravityContext(
gravityContext,
finiteElements,
densityPlus,
displacement,
dependencies
gravityContext, finiteElements, densityPlus, displacement, dependencies
);
operation.Prepare(angularVelocity, dependencies);
const double densityPlusResidual = angular_momentum_test_utils::residual(operation);
@@ -384,15 +345,11 @@ TEST_CASE(
densityMinus.Add(-densityStep, densityDirection);
++dependencies.density.revision;
angular_momentum_test_utils::prepareGravityContext(
gravityContext,
finiteElements,
densityMinus,
displacement,
dependencies
gravityContext, finiteElements, densityMinus, displacement, dependencies
);
operation.Prepare(angularVelocity, dependencies);
const double densityMinusResidual = angular_momentum_test_utils::residual(operation);
const double densityDifference = (densityPlusResidual - densityMinusResidual) / (2.0 * densityStep);
const double densityDifference = (densityPlusResidual - densityMinusResidual) / (2.0 * densityStep);
CHECK(angular_momentum_test_utils::relativeError(densityAction(0), densityDifference) < 4.0e-8);
constexpr double geometryStep = 1.0e-6;
@@ -401,11 +358,7 @@ TEST_CASE(
++dependencies.density.revision;
++dependencies.displacement.revision;
angular_momentum_test_utils::prepareGravityContext(
gravityContext,
finiteElements,
density,
displacementPlus,
dependencies
gravityContext, finiteElements, density, displacementPlus, dependencies
);
operation.Prepare(angularVelocity, dependencies);
const double geometryPlusResidual = angular_momentum_test_utils::residual(operation);
@@ -413,19 +366,19 @@ TEST_CASE(
displacementMinus.Add(-geometryStep, displacementDirection);
++dependencies.displacement.revision;
angular_momentum_test_utils::prepareGravityContext(
gravityContext,
finiteElements,
density,
displacementMinus,
dependencies
gravityContext, finiteElements, density, displacementMinus, dependencies
);
operation.Prepare(angularVelocity, dependencies);
const double geometryMinusResidual = angular_momentum_test_utils::residual(operation);
const double geometryDifference = (geometryPlusResidual - geometryMinusResidual) / (2.0 * geometryStep);
INFO("Density angular-momentum derivative error = " <<
angular_momentum_test_utils::relativeError(densityAction(0), densityDifference));
INFO("Geometry angular-momentum derivative error = " <<
angular_momentum_test_utils::relativeError(geometryAction(0), geometryDifference));
const double geometryDifference = (geometryPlusResidual - geometryMinusResidual) / (2.0 * geometryStep);
INFO(
"Density angular-momentum derivative error = "
<< angular_momentum_test_utils::relativeError(densityAction(0), densityDifference)
);
INFO(
"Geometry angular-momentum derivative error = "
<< angular_momentum_test_utils::relativeError(geometryAction(0), geometryDifference)
);
CHECK(angular_momentum_test_utils::relativeError(geometryAction(0), geometryDifference) < 4.0e-7);
}
@@ -435,53 +388,35 @@ TEST_CASE(
) {
using namespace mean_field;
utils::Args arguments = test_utils::setup_args();
utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
mfem::Vector density = angular_momentum_test_utils::projectDensity(finiteElements, 0.29);
mfem::Vector displacement =
angular_momentum_test_utils::projectDisplacementDirection(finiteElements, 0.41);
auto dependencies = angular_momentum_test_utils::makeDependencies();
mfem::Vector density = angular_momentum_test_utils::projectDensity(finiteElements, 0.29);
mfem::Vector displacement = angular_momentum_test_utils::projectDisplacementDirection(finiteElements, 0.41);
auto dependencies = angular_momentum_test_utils::makeDependencies();
std::uint64_t gravityPotentialRevision = 17;
operators::context::gravity_field::GravityFieldLinearizationContext gravityContext(
finiteElements,
*finiteElements.domainMapperStateless
finiteElements, *finiteElements.domainMapperStateless
);
angular_momentum_test_utils::prepareGravityContext(
gravityContext,
finiteElements,
density,
displacement,
dependencies,
13,
gravityPotentialRevision
gravityContext, finiteElements, density, displacement, dependencies, 13, gravityPotentialRevision
);
operators::PreparedAngularMomentumOperator operation(
finiteElements,
*finiteElements.domainMapperStateless,
gravityContext,
models::compileConstraint(
integral::FixedAngularMomentum({.Jtotal = dimensions::AngularMomentumValue{0.71}})
)
finiteElements, *finiteElements.domainMapperStateless, gravityContext,
models::compileConstraint(integral::FixedAngularMomentum({.Jtotal = dimensions::AngularMomentumValue{0.71}}))
);
operation.Prepare(0.52, dependencies);
const auto preparationCount = operation.GetPreparationCount();
const double moment = operation.GetMomentOfInertia();
const double moment = operation.GetMomentOfInertia();
const auto repeated = operation.Prepare(0.52, dependencies);
const auto repeated = operation.Prepare(0.52, dependencies);
CHECK_FALSE(repeated.DidAnyWork());
CHECK(operation.GetPreparationCount() == preparationCount);
++gravityPotentialRevision;
angular_momentum_test_utils::prepareGravityContext(
gravityContext,
finiteElements,
density,
displacement,
dependencies,
13,
gravityPotentialRevision
gravityContext, finiteElements, density, displacement, dependencies, 13, gravityPotentialRevision
);
const auto unrelatedPotential = operation.Prepare(0.52, dependencies);
CHECK_FALSE(unrelatedPotential.DidAnyWork());
@@ -494,19 +429,15 @@ TEST_CASE(
CHECK_FALSE(rotationOnly.refreshedDensity);
CHECK_FALSE(rotationOnly.refreshedGeometry);
CHECK(operation.GetMomentOfInertia() == moment);
CHECK(angular_momentum_test_utils::residual(operation) - residualBeforeRotation ==
Catch::Approx((0.81 - 0.52) * moment).epsilon(3.0e-15));
CHECK(
angular_momentum_test_utils::residual(operation) - residualBeforeRotation ==
Catch::Approx((0.81 - 0.52) * moment).epsilon(3.0e-15)
);
density = angular_momentum_test_utils::projectDensity(finiteElements, 0.83);
++dependencies.density.revision;
angular_momentum_test_utils::prepareGravityContext(
gravityContext,
finiteElements,
density,
displacement,
dependencies,
13,
gravityPotentialRevision
gravityContext, finiteElements, density, displacement, dependencies, 13, gravityPotentialRevision
);
const auto densityOnly = operation.Prepare(0.81, dependencies);
CHECK(densityOnly.refreshedDensity);
@@ -516,16 +447,84 @@ TEST_CASE(
displacement = angular_momentum_test_utils::projectDisplacementDirection(finiteElements, 0.87);
++dependencies.displacement.revision;
angular_momentum_test_utils::prepareGravityContext(
gravityContext,
finiteElements,
density,
displacement,
dependencies,
13,
gravityPotentialRevision
gravityContext, finiteElements, density, displacement, dependencies, 13, gravityPotentialRevision
);
const auto geometryOnly = operation.Prepare(0.81, dependencies);
CHECK(geometryOnly.refreshedGeometry);
CHECK_FALSE(geometryOnly.refreshedDensity);
CHECK_FALSE(geometryOnly.updatedAngularVelocity);
}
TEST_CASE(
"Prepared Angular Momentum Returns Explicit Candidate Rejections And Recovers",
"[fixed-angular-momentum][prepared][trial-outcome]"
) {
using namespace mean_field;
STATIC_CHECK(std::is_trivially_copyable_v<operators::AngularMomentumPreparationRejection>);
utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
constexpr double angularVelocity = 0.52;
const mfem::Vector positiveDensity = angular_momentum_test_utils::projectConstantDensity(finiteElements, 0.91);
const mfem::Vector negativeDensity = angular_momentum_test_utils::projectConstantDensity(finiteElements, -0.91);
mfem::Vector displacement(finiteElements.displacementFes->GetTrueVSize());
displacement = 0.0;
auto dependencies = angular_momentum_test_utils::makeDependencies();
operators::context::gravity_field::GravityFieldLinearizationContext gravityContext(
finiteElements, *finiteElements.domainMapperStateless
);
angular_momentum_test_utils::prepareGravityContext(
gravityContext, finiteElements, positiveDensity, displacement, dependencies
);
operators::PreparedAngularMomentumOperator operation(
finiteElements, *finiteElements.domainMapperStateless, gravityContext,
models::compileConstraint(integral::FixedAngularMomentum({.Jtotal = dimensions::AngularMomentumValue{0.71}}))
);
const auto initial = operation.TryPrepare(angularVelocity, dependencies);
REQUIRE(initial.has_value());
const std::uint64_t initialPreparationCount = operation.GetPreparationCount();
const std::uint64_t successfulPreparations = initialPreparationCount;
++dependencies.density.revision;
angular_momentum_test_utils::prepareGravityContext(
gravityContext, finiteElements, negativeDensity, displacement, dependencies
);
const auto negativeMoment = operation.TryPrepare(angularVelocity, dependencies);
REQUIRE_FALSE(negativeMoment.has_value());
CHECK(
negativeMoment.error().reason ==
operators::AngularMomentumPreparationRejectionReason::negative_moment_of_inertia
);
CHECK(negativeMoment.error().momentOfInertia < 0.0);
CHECK(operation.GetPreparationCount() == successfulPreparations);
CHECK_FALSE(operation.IsPrepared());
REQUIRE_THROWS_AS(operation.Prepare(angularVelocity, dependencies), std::domain_error);
++dependencies.density.revision;
angular_momentum_test_utils::prepareGravityContext(
gravityContext, finiteElements, positiveDensity, displacement, dependencies
);
const auto recovered = operation.TryPrepare(angularVelocity, dependencies);
REQUIRE(recovered.has_value());
CHECK(operation.IsPrepared());
++dependencies.rotation.revision;
const auto nonFiniteAngularVelocity = operation.TryPrepare(std::numeric_limits<double>::quiet_NaN(), dependencies);
REQUIRE_FALSE(nonFiniteAngularVelocity.has_value());
CHECK(
nonFiniteAngularVelocity.error().reason ==
operators::AngularMomentumPreparationRejectionReason::non_finite_angular_velocity
);
CHECK_FALSE(operation.IsPrepared());
REQUIRE_THROWS_AS(operation.Prepare(std::numeric_limits<double>::quiet_NaN(), dependencies), std::domain_error);
++dependencies.rotation.revision;
REQUIRE(operation.TryPrepare(angularVelocity, dependencies).has_value());
CHECK(operation.IsPrepared());
}

View File

@@ -6,6 +6,7 @@
#include <cstdint>
#include <limits>
#include <mfem.hpp>
#include <stdexcept>
#include <type_traits>
import mean_field;
@@ -101,6 +102,22 @@ namespace prepared_barotropic_closure_test_utils {
return project_scalar(finiteElementSpace, coefficient);
}
[[nodiscard]] mfem::Vector make_folding_displacement(const mean_field::fem::FEM &f) {
mfem::ParGridFunction fieldValue(f.displacementFes.get());
mfem::VectorFunctionCoefficient coefficient(
f.mesh->Dimension(), [](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(position.Size());
value = 0.0;
value(0) = -2.0 * position(0);
}
);
fieldValue.ProjectCoefficient(coefficient);
mfem::Vector result;
fieldValue.GetTrueDofs(result);
return result;
}
[[nodiscard]] mfem::Vector reduce(
const field::FieldDofMap &map,
const mfem::Vector &full
@@ -263,6 +280,7 @@ namespace prepared_barotropic_closure_test_utils {
STATIC_REQUIRE_FALSE(std::is_copy_assignable_v<Operator>);
STATIC_REQUIRE_FALSE(std::is_move_constructible_v<Operator>);
STATIC_REQUIRE_FALSE(std::is_move_assignable_v<Operator>);
STATIC_REQUIRE(std::is_trivially_copyable_v<mean_field::operators::BarotropicClosurePreparationRejection>);
auto args = test_utils::setup_args();
mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0);
@@ -303,6 +321,115 @@ namespace prepared_barotropic_closure_test_utils {
CHECK(globalEnthalpyReduced < globalEnthalpyFull);
}
TEST_CASE(
"Prepared Barotropic Closure Reports Expected EOS Rejections Without Unwinding",
tags::barotrope &tags::closure &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);
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
);
mfem::Vector density(maps.density.reduced_size());
mfem::Vector enthalpy(maps.enthalpy.reduced_size());
mfem::Vector displacement(maps.displacement.reduced_size());
density = 0.0;
enthalpy = -1.0;
displacement = 0.0;
auto dependencies = make_dependencies();
const auto outsideDomain =
preparedOperator.TryPrepare(make_state_view(density, enthalpy, displacement), dependencies);
REQUIRE_FALSE(outsideDomain.has_value());
CHECK(
outsideDomain.error().reason ==
mean_field::operators::BarotropicClosurePreparationRejectionReason::equation_of_state
);
CHECK(outsideDomain.error().equationOfStateError == mean_field::eos::EvaluationErrorCode::outside_domain);
CHECK_FALSE(preparedOperator.IsPrepared());
try {
(void)preparedOperator.Prepare(make_state_view(density, enthalpy, displacement), dependencies);
FAIL("The compatibility Prepare overload accepted an out-of-domain EOS input.");
} catch (const mean_field::eos::EvaluationError &error) {
CHECK(error.code() == mean_field::eos::EvaluationErrorCode::outside_domain);
}
// Keep the interpolated input finite while forcing the n = 3
// polytropic density evaluation to overflow.
enthalpy = 1.0e150;
++dependencies.enthalpy.revision;
const auto rejected =
preparedOperator.TryPrepare(make_state_view(density, enthalpy, displacement), dependencies);
REQUIRE_FALSE(rejected.has_value());
CHECK(
rejected.error().reason ==
mean_field::operators::BarotropicClosurePreparationRejectionReason::equation_of_state
);
CHECK(rejected.error().equationOfStateError == mean_field::eos::EvaluationErrorCode::nonfinite_result);
CHECK_FALSE(preparedOperator.IsPrepared());
try {
(void)preparedOperator.Prepare(make_state_view(density, enthalpy, displacement), dependencies);
FAIL("The compatibility Prepare overload accepted a non-finite EOS result.");
} catch (const mean_field::eos::EvaluationError &error) {
CHECK(error.code() == mean_field::eos::EvaluationErrorCode::nonfinite_result);
}
enthalpy = 1.0;
++dependencies.enthalpy.revision;
const auto accepted =
preparedOperator.TryPrepare(make_state_view(density, enthalpy, displacement), dependencies);
REQUIRE(accepted.has_value());
CHECK(preparedOperator.IsPrepared());
}
TEST_CASE(
"Prepared Barotropic Closure Reports Invalid Candidate Geometry Without Unwinding",
tags::barotrope &tags::closure &tags::prepared &tags::geometry &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.okay());
const Maps maps(f);
const mean_field::eos::Polytrope equationOfState(3.0, 1.5);
mean_field::operators::PreparedBarotropicClosureOperator preparedOperator(
f, *f.domainMapperStateless, equationOfState
);
mfem::Vector density(maps.density.reduced_size());
mfem::Vector enthalpy(maps.enthalpy.reduced_size());
density = 1.0;
enthalpy = 1.0;
mfem::Vector displacement = reduce(maps.displacement, make_folding_displacement(f));
auto dependencies = make_dependencies();
const auto rejected =
preparedOperator.TryPrepare(make_state_view(density, enthalpy, displacement), dependencies);
REQUIRE_FALSE(rejected.has_value());
CHECK(
rejected.error().reason ==
mean_field::operators::BarotropicClosurePreparationRejectionReason::mapping_failure
);
CHECK(rejected.error().mappingStatus == mean_field::mapping::MappingStatus::non_positive_determinant);
CHECK_FALSE(preparedOperator.IsPrepared());
CHECK_THROWS_AS(
preparedOperator.Prepare(make_state_view(density, enthalpy, displacement), dependencies), std::domain_error
);
displacement = 0.0;
++dependencies.displacement.revision;
const auto accepted =
preparedOperator.TryPrepare(make_state_view(density, enthalpy, displacement), dependencies);
REQUIRE(accepted.has_value());
CHECK(preparedOperator.IsPrepared());
}
TEST_CASE(
"Prepared Barotropic Closure Matches Full Stateless Kernels Through FieldDof Restriction",
tags::barotrope &tags::closure &tags::hydro &tags::prepared &tags::field &tags::integration

View File

@@ -2,7 +2,10 @@
#include <cmath>
#include <concepts>
#include <cstdint>
#include <limits>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
@@ -49,24 +52,30 @@ TEST_CASE(
) {
using namespace mean_field;
utils::Args args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.okay());
utils::Args args = test_utils::setup_args();
fem::FEM physicalFiniteElements = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(physicalFiniteElements.okay());
fem::FEM borderedFiniteElements = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(borderedFiniteElements.okay());
models::StellarModel stellarModel{
models::structure::PolytropicStructure{eos::Polytrope{3.0, 0.25}, 1.0},
surface::ConstantPressureSurface{dimensions::PressureValue{0.0}}
};
operators::PreparedStellarEquilibriumOperator physicalOperator(f, *f.domainMapperStateless, stellarModel);
operators::PreparedStellarEquilibriumOperator physicalOperator(
physicalFiniteElements, *physicalFiniteElements.domainMapperStateless, stellarModel
);
auto equilibriumProblem = equilibrium::discretize(
model::StellarModel(
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{1.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}), eos::Polytrope({.n = 3.0, .K = 0.25})
),
equilibrium::StellarDiscretization{f, *f.domainMapperStateless}
std::move(borderedFiniteElements)
);
auto &borderedOperator = equilibriumProblem.GetPreparedOperator();
auto &borderedOperator = equilibriumProblem.GetPreparedOperator();
const MPI_Comm communicator = equilibriumProblem.GetCommunicator();
STATIC_CHECK(
std::same_as<
@@ -87,8 +96,7 @@ TEST_CASE(
CHECK(borderedOperator.GetRootManifest().constraints().size() == 3);
CHECK(borderedOperator.GetRootManifest().specificationDescriptors().size() == 4);
const auto &centralDescriptor =
borderedOperator.GetRootManifest().specification<constraint::FixedCentralDensity>();
const auto &centralDescriptor = borderedOperator.GetRootManifest().specification<constraint::FixedCentralDensity>();
CHECK(centralDescriptor.stableId == "FixedCentralDensity");
CHECK(centralDescriptor.role == models::SpecificationRole::phase_condition);
CHECK(centralDescriptor.columnPolicy == operators::RootColumnPolicy::solver_border);
@@ -99,10 +107,9 @@ TEST_CASE(
CHECK(centralDescriptor.residualUnits == "specific_enthalpy");
mfem::Vector physicalState(physicalOperator.Width());
physicalState = 0.0;
const auto physicalStateView =
physicalOperator.GetRootManifest().stateView(physicalState);
physicalStateView.block(utils::blocks::density_field.mass_term) = 1.0;
physicalState = 0.0;
const auto physicalStateView = physicalOperator.GetRootManifest().stateView(physicalState);
physicalStateView.block(utils::blocks::density_field.mass_term) = 1.0;
physicalStateView.block(utils::blocks::enthalpy_field.specific_term) = 1.0;
mfem::Vector borderedState(borderedOperator.Width());
@@ -152,7 +159,7 @@ TEST_CASE(
localCenterDirection += enthalpyDirection(centerDof);
}
double globalCenterDirection = 0.0;
MPI_Allreduce(&localCenterDirection, &globalCenterDirection, 1, MPI_DOUBLE, MPI_SUM, f.mesh->GetComm());
MPI_Allreduce(&localCenterDirection, &globalCenterDirection, 1, MPI_DOUBLE, MPI_SUM, communicator);
CHECK(borderedAction(borderedAction.Size() - 1) == globalCenterDirection);
const auto repeatedReport = borderedOperator.Prepare(borderedState, dependencies, rotation);
@@ -181,6 +188,60 @@ TEST_CASE(
localBorderEntry += enthalpyAction(centerDof);
}
double globalBorderEntry = 0.0;
MPI_Allreduce(&localBorderEntry, &globalBorderEntry, 1, MPI_DOUBLE, MPI_SUM, f.mesh->GetComm());
MPI_Allreduce(&localBorderEntry, &globalBorderEntry, 1, MPI_DOUBLE, MPI_SUM, communicator);
CHECK(globalBorderEntry == -0.625);
}
TEST_CASE(
"Central Density Variadic Preparation Rejects A Non-Finite Phase Coordinate Without Unwinding",
tags::central_density_phase_integration
) {
using namespace mean_field;
utils::Args args = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(finiteElements.okay());
auto equilibriumProblem = equilibrium::discretize(
model::StellarModel(
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{1.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}), eos::Polytrope({.n = 3.0, .K = 0.25})
),
std::move(finiteElements)
);
auto &preparedOperator = equilibriumProblem.GetPreparedOperator();
mfem::Vector state(preparedOperator.Width());
state = 0.0;
const auto stateView = preparedOperator.GetRootManifest().stateView(state);
stateView.block(utils::blocks::density_field.mass_term) = 1.0;
stateView.block(utils::blocks::enthalpy_field.specific_term) = 1.0;
const operators::StellarEquilibriumDependencies dependencies = make_dependencies();
const physics::RigidRotation rotation = make_zero_rotation();
REQUIRE(equilibriumProblem.TryPrepare(state, dependencies, rotation).has_value());
int rank = 0;
REQUIRE(MPI_Comm_rank(equilibriumProblem.GetCommunicator(), &rank) == MPI_SUCCESS);
auto phaseCoordinate = preparedOperator.GetRootManifest().stateView(state).block(
utils::blocks::fixed_central_density_phase.central_value_term
);
REQUIRE(phaseCoordinate.Size() == 1);
if (rank == 0) {
phaseCoordinate(0) = std::numeric_limits<double>::quiet_NaN();
phaseCoordinate.SyncAliasMemory(state);
}
const auto rejected = equilibriumProblem.TryPrepare(state, dependencies, rotation);
REQUIRE_FALSE(rejected.has_value());
CHECK(rejected.error().reason == operators::StellarEquilibriumPreparationRejectionReason::non_finite_physics);
CHECK(rejected.error().stage == operators::StellarEquilibriumPreparationStage::model_specification);
CHECK_FALSE(equilibriumProblem.IsPrepared());
CHECK_THROWS_AS(equilibriumProblem.Prepare(state, dependencies, rotation), std::domain_error);
phaseCoordinate(0) = 0.0;
phaseCoordinate.SyncAliasMemory(state);
REQUIRE(equilibriumProblem.TryPrepare(state, dependencies, rotation).has_value());
CHECK(equilibriumProblem.IsPrepared());
}

View File

@@ -415,6 +415,57 @@ TEST_CASE(
CHECK(preparedOperator.IsPrepared());
}
TEST_CASE(
"Prepared Displacement Residual Preserves Pressure Rejection Details",
tags::barotrope_prepared
) {
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 mfem::Vector density = prepared_displacement_residual_test_utils::make_density(f, 0.31);
const mfem::Vector displacement = gravity_prepared_test_utils::make_displacement(f, 0.67);
const mfem::Vector gravityGradient = prepared_displacement_residual_test_utils::make_gravity_gradient(f, 0.47);
mfem::Vector gravityPotential(f.gravityPotentialFes->GetTrueVSize());
gravityPotential = 0.0;
auto dependencies = prepared_displacement_residual_test_utils::make_dependencies();
mean_field::operators::context::gravity_field::GravityFieldLinearizationContext gravityContext(
f, *f.domainMapperStateless
);
prepared_displacement_residual_test_utils::prepare_gravity_context(
gravityContext, density, displacement, gravityGradient, gravityPotential, dependencies, 19
);
const mean_field::eos::Polytrope equationOfState(3.0, 0.25);
const mean_field::physics::RigidRotation rotation = prepared_displacement_residual_test_utils::make_rotation(0.83);
mean_field::operators::PreparedDisplacementResidualOperator preparedOperator(
f, *f.domainMapperStateless, equationOfState, gravityContext
);
mfem::Vector enthalpy(prepared_displacement_residual_test_utils::make_enthalpy_map(f).reduced_size());
enthalpy = std::numeric_limits<double>::max();
const auto rejected = preparedOperator.TryPrepare({.enthalpy = enthalpy}, dependencies, rotation);
REQUIRE_FALSE(rejected.has_value());
CHECK(rejected.error().source == mean_field::operators::DisplacementResidualPreparationRejectionSource::pressure);
CHECK(
rejected.error().reason ==
mean_field::operators::DisplacementResidualPreparationRejectionReason::equation_of_state
);
CHECK(rejected.error().equationOfStateCode == mean_field::eos::EvaluationErrorCode::nonfinite_result);
CHECK_FALSE(preparedOperator.IsPrepared());
CHECK_THROWS_AS(
preparedOperator.Prepare({.enthalpy = enthalpy}, dependencies, rotation), mean_field::eos::EvaluationError
);
enthalpy = 1.0;
++dependencies.enthalpy.revision;
const auto accepted = preparedOperator.TryPrepare({.enthalpy = enthalpy}, dependencies, rotation);
REQUIRE(accepted.has_value());
CHECK(preparedOperator.IsPrepared());
}
TEST_CASE(
"Prepared Displacement Residual Selectively Orchestrates Its Children",
tags::barotrope_context_integration

View File

@@ -1,6 +1,7 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <mfem.hpp>
#include <stdexcept>
import mean_field;
import test_helpers;
@@ -9,6 +10,56 @@ using namespace mean_field;
using Catch::Matchers::WithinAbs;
namespace prepared_test = gravity_prepared_test_utils;
namespace {
[[nodiscard]] mfem::Vector make_folding_displacement(const mean_field::fem::FEM &f) {
mfem::ParGridFunction field(f.displacementFes.get());
mfem::VectorFunctionCoefficient coefficient(
f.mesh->Dimension(), [](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(position.Size());
for (int dimension = 0; dimension < position.Size(); ++dimension) {
value(dimension) = -2.0 * position(dimension);
}
}
);
field.ProjectCoefficient(coefficient);
mfem::Vector displacementTrue;
field.GetTrueDofs(displacementTrue);
return displacementTrue;
}
} // namespace
TEST_CASE(
"Prepared Mapped Gravity Source Reports Invalid Candidate Geometry Without Unwinding",
tags::gravity_prepared_unit &tags::geometry
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.okay());
operators::PreparedMappedGravitySourceOperator preparedOperator(f, *f.domainMapperStateless);
mfem::Vector displacement = preparedOperator.GetDisplacementMap().gather(make_folding_displacement(f));
const auto rejected = preparedOperator.TryPrepare(displacement);
REQUIRE_FALSE(rejected.has_value());
CHECK(rejected.error().reason == operators::GravitySourcePreparationRejectionReason::invalid_mapping);
CHECK(rejected.error().mappingStatus == mapping::MappingStatus::non_positive_determinant);
CHECK_FALSE(preparedOperator.IsPrepared());
CHECK_FALSE(preparedOperator.HasVariationData());
CHECK(preparedOperator.GetPreparationCount() == 0);
CHECK_THROWS_AS(preparedOperator.Prepare(displacement), std::domain_error);
CHECK_FALSE(preparedOperator.IsPrepared());
CHECK(preparedOperator.GetPreparationCount() == 0);
displacement = 0.0;
const auto recovered = preparedOperator.TryPrepare(displacement);
REQUIRE(recovered.has_value());
CHECK(preparedOperator.IsPrepared());
CHECK(preparedOperator.HasVariationData());
CHECK(preparedOperator.GetPreparationCount() == 1);
}
TEST_CASE(
"Prepared Mapped Gravity Source Matches Stateless Kernel",
tags::gravity_prepared

View File

@@ -2,6 +2,7 @@
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <cmath>
#include <mfem.hpp>
#include <stdexcept>
import mean_field;
import test_helpers;
@@ -10,6 +11,56 @@ using namespace mean_field;
using Catch::Matchers::WithinAbs;
namespace prepared_test = gravity_prepared_test_utils;
namespace {
[[nodiscard]] mfem::Vector make_folding_displacement(const mean_field::fem::FEM &f) {
mfem::ParGridFunction field(f.displacementFes.get());
mfem::VectorFunctionCoefficient coefficient(
f.mesh->Dimension(), [](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(position.Size());
for (int dimension = 0; dimension < position.Size(); ++dimension) {
value(dimension) = -2.0 * position(dimension);
}
}
);
field.ProjectCoefficient(coefficient);
mfem::Vector displacementTrue;
field.GetTrueDofs(displacementTrue);
return displacementTrue;
}
} // namespace
TEST_CASE(
"Prepared Mapped Hdiv Mass Reports Invalid Candidate Geometry Without Unwinding",
tags::gravity_prepared_unit &tags::geometry
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.okay());
operators::PreparedMappedHDivMassOperator preparedOperator(f, *f.domainMapperStateless);
mfem::Vector displacement = preparedOperator.GetDisplacementMap().gather(make_folding_displacement(f));
const auto rejected = preparedOperator.TryPrepare(displacement);
REQUIRE_FALSE(rejected.has_value());
CHECK(rejected.error().reason == operators::HDivMassPreparationRejectionReason::invalid_mapping);
CHECK(rejected.error().mappingStatus == mapping::MappingStatus::non_positive_determinant);
CHECK_FALSE(preparedOperator.IsPrepared());
CHECK_FALSE(preparedOperator.HasVariationData());
CHECK(preparedOperator.GetPreparationCount() == 0);
CHECK_THROWS_AS(preparedOperator.Prepare(displacement), std::domain_error);
CHECK_FALSE(preparedOperator.IsPrepared());
CHECK(preparedOperator.GetPreparationCount() == 0);
displacement = 0.0;
const auto recovered = preparedOperator.TryPrepare(displacement);
REQUIRE(recovered.has_value());
CHECK(preparedOperator.IsPrepared());
CHECK(preparedOperator.HasVariationData());
CHECK(preparedOperator.GetPreparationCount() == 1);
}
TEST_CASE(
"Prepared Mapped Hdiv Mass Matches Stateless Kernel",
tags::gravity_prepared

View File

@@ -1,5 +1,6 @@
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
#include <stdexcept>
import mean_field;
import test_helpers;
@@ -288,3 +289,75 @@ TEST_CASE(
CHECK(displacementEffect > 1.0e-8);
}
TEST_CASE(
"Prepared Hydrostatic Equilibrium Reports Candidate Mapping And Arithmetic Failures Without Unwinding",
tags::barotrope_hydrostatic_prepared_residual &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.okay());
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 = field_dof_test_utils::make_supported_displacement(f, 0.73);
displacement *= 1.0e200;
auto dependencies = prepared_hydrostatic_test_utils::make_dependencies();
const auto rotation = prepared_hydrostatic_test_utils::make_rotation();
const auto mappingRejection = preparedOperator.TryPrepare(
prepared_hydrostatic_test_utils::make_state(enthalpy, gravityPotential, displacement, 0.41), dependencies,
rotation
);
REQUIRE_FALSE(mappingRejection.has_value());
CHECK(
(mappingRejection.error().reason ==
mean_field::operators::HydrostaticEquilibriumPreparationRejectionReason::inverted_geometry ||
mappingRejection.error().reason ==
mean_field::operators::HydrostaticEquilibriumPreparationRejectionReason::non_finite_geometry)
);
CHECK(mappingRejection.error().mappingStatus != mean_field::mapping::MappingStatus::valid);
CHECK_FALSE(preparedOperator.IsPrepared());
CHECK_THROWS_AS(
preparedOperator.Prepare(
prepared_hydrostatic_test_utils::make_state(enthalpy, gravityPotential, displacement, 0.41), dependencies,
rotation
),
std::domain_error
);
displacement = 0.0;
++dependencies.displacement.revision;
++dependencies.rotation.revision;
const auto enormousRotation = prepared_hydrostatic_test_utils::make_rotation(1.0e200);
const auto arithmeticRejection = preparedOperator.TryPrepare(
prepared_hydrostatic_test_utils::make_state(enthalpy, gravityPotential, displacement, 0.41), dependencies,
enormousRotation
);
REQUIRE_FALSE(arithmeticRejection.has_value());
CHECK(
arithmeticRejection.error().reason ==
mean_field::operators::HydrostaticEquilibriumPreparationRejectionReason::non_finite_residual
);
CHECK_FALSE(preparedOperator.IsPrepared());
CHECK_THROWS_AS(
preparedOperator.Prepare(
prepared_hydrostatic_test_utils::make_state(enthalpy, gravityPotential, displacement, 0.41), dependencies,
enormousRotation
),
std::domain_error
);
++dependencies.rotation.revision;
const auto recovered = preparedOperator.TryPrepare(
prepared_hydrostatic_test_utils::make_state(enthalpy, gravityPotential, displacement, 0.41), dependencies,
rotation
);
REQUIRE(recovered.has_value());
CHECK(recovered->preparedResidual);
CHECK(preparedOperator.IsPrepared());
}

View File

@@ -3,6 +3,7 @@
#include <cmath>
#include <cstdint>
#include <limits>
#include <stdexcept>
#include <type_traits>
#include <catch2/catch_test_macros.hpp>
@@ -205,6 +206,7 @@ TEST_CASE(
STATIC_REQUIRE_FALSE(std::is_copy_assignable_v<Operator>);
STATIC_REQUIRE_FALSE(std::is_move_constructible_v<Operator>);
STATIC_REQUIRE_FALSE(std::is_move_assignable_v<Operator>);
STATIC_REQUIRE(std::is_trivially_copyable_v<mean_field::operators::MassNormalizationPreparationRejection>);
mean_field::utils::Args args = test_utils::setup_args();
mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0);
@@ -271,6 +273,49 @@ TEST_CASE(
CHECK(mass_normalization_test_utils::relative_error(measuredScale, expectedScale) < 5e-7);
}
TEST_CASE(
"Prepared Mass Normalization Reports Non-Finite Density Interpolation Without Unwinding",
tags::barotrope_mass_normalization_context &tags::unit
) {
using namespace mass_normalization_test_utils;
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());
mfem::Vector density = make_constant_density(f, -0.5 * std::numeric_limits<double>::max());
mfem::Vector displacement(f.displacementFes->GetTrueVSize());
displacement = 0.0;
auto dependencies = make_dependencies();
mean_field::operators::context::gravity_field::GravityFieldLinearizationContext gravityContext(
f, *f.domainMapperStateless
);
prepare_gravity_context(gravityContext, f, density, displacement, dependencies);
mean_field::operators::PreparedMassNormalizationOperator massOperator(f, *f.domainMapperStateless, gravityContext);
const auto rejected = massOperator.TryPrepare({.targetMass = std::numeric_limits<double>::max()}, dependencies);
REQUIRE_FALSE(rejected.has_value());
CHECK(
rejected.error().reason ==
mean_field::operators::MassNormalizationPreparationRejectionReason::non_finite_density_interpolation
);
CHECK_FALSE(massOperator.IsPrepared());
CHECK_THROWS_AS(
massOperator.Prepare({.targetMass = std::numeric_limits<double>::max()}, dependencies), std::domain_error
);
density = make_constant_density(f, 1.0);
++dependencies.density.revision;
++dependencies.targetMass.revision;
prepare_gravity_context(gravityContext, f, density, displacement, dependencies);
const auto accepted = massOperator.TryPrepare({.targetMass = 1.0}, dependencies);
REQUIRE(accepted.has_value());
CHECK(massOperator.IsPrepared());
}
TEST_CASE(
"Prepared Mass Normalization Density Jacobian Matches Centered Difference",
tags::barotrope_mass_normalization_jacobian &tags::accuracy

View File

@@ -3,6 +3,7 @@
#include <cmath>
#include <limits>
#include <memory>
#include <stdexcept>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
@@ -51,6 +52,26 @@ namespace prepared_pressure_force_test_utils {
}
};
[[nodiscard]] mfem::Vector make_affine_displacement(
const mean_field::fem::FEM &f,
const double scale
) {
mfem::ParGridFunction field(f.displacementFes.get());
mfem::VectorFunctionCoefficient coefficient(
f.mesh->Dimension(), [scale](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(position.Size());
for (int dimension = 0; dimension < position.Size(); ++dimension) {
value(dimension) = scale * position(dimension);
}
}
);
field.ProjectCoefficient(coefficient);
mfem::Vector result;
field.GetTrueDofs(result);
return result;
}
[[nodiscard]]
mfem::Vector make_positive_enthalpy_true(
const mean_field::fem::FEM &f,
@@ -265,6 +286,97 @@ TEST_CASE(
);
}
TEST_CASE(
"Prepared Pressure Force Reports Expected EOS Rejections Without Unwinding",
tags::barotrope &tags::pressure &tags::prepared &tags::unit
) {
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 prepared_pressure_force_test_utils::Maps maps(f);
const mean_field::eos::Polytrope equationOfState(3.0, 0.25);
mean_field::operators::PreparedPressureForceOperator preparedOperator(f, *f.domainMapperStateless, equationOfState);
mfem::Vector enthalpy(maps.enthalpy.reduced_size());
mfem::Vector displacement(maps.displacement.reduced_size());
enthalpy = -1.0;
displacement = 0.0;
auto dependencies = prepared_pressure_force_test_utils::make_dependencies();
const auto outsideDomain =
preparedOperator.TryPrepare({.enthalpy = enthalpy, .displacement = displacement}, dependencies);
REQUIRE_FALSE(outsideDomain.has_value());
CHECK(
outsideDomain.error().reason ==
mean_field::operators::PressureForcePreparationRejectionReason::equation_of_state
);
CHECK(outsideDomain.error().equationOfStateCode == mean_field::eos::EvaluationErrorCode::outside_domain);
CHECK_FALSE(preparedOperator.IsPrepared());
try {
(void)preparedOperator.Prepare({.enthalpy = enthalpy, .displacement = displacement}, dependencies);
FAIL("The compatibility Prepare overload accepted an out-of-domain EOS input.");
} catch (const mean_field::eos::EvaluationError &error) {
CHECK(error.code() == mean_field::eos::EvaluationErrorCode::outside_domain);
}
enthalpy = std::numeric_limits<double>::max();
++dependencies.enthalpy.revision;
const auto rejected =
preparedOperator.TryPrepare({.enthalpy = enthalpy, .displacement = displacement}, dependencies);
REQUIRE_FALSE(rejected.has_value());
CHECK(rejected.error().reason == mean_field::operators::PressureForcePreparationRejectionReason::equation_of_state);
CHECK(rejected.error().equationOfStateCode == mean_field::eos::EvaluationErrorCode::nonfinite_result);
CHECK_FALSE(preparedOperator.IsPrepared());
CHECK_THROWS_AS(
preparedOperator.Prepare({.enthalpy = enthalpy, .displacement = displacement}, dependencies),
mean_field::eos::EvaluationError
);
enthalpy = 1.0;
++dependencies.enthalpy.revision;
const auto accepted =
preparedOperator.TryPrepare({.enthalpy = enthalpy, .displacement = displacement}, dependencies);
REQUIRE(accepted.has_value());
CHECK(preparedOperator.IsPrepared());
}
TEST_CASE(
"Prepared Pressure Force Reports Invalid Candidate Geometry Without Unwinding",
tags::barotrope &tags::pressure &tags::prepared &tags::geometry &tags::unit
) {
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 prepared_pressure_force_test_utils::Maps maps(f);
const mean_field::eos::Polytrope equationOfState(3.0, 0.25);
mean_field::operators::PreparedPressureForceOperator preparedOperator(f, *f.domainMapperStateless, equationOfState);
mfem::Vector enthalpy(maps.enthalpy.reduced_size());
enthalpy = 1.0;
mfem::Vector displacement =
maps.displacement.gather(prepared_pressure_force_test_utils::make_affine_displacement(f, -2.0));
auto dependencies = prepared_pressure_force_test_utils::make_dependencies();
const auto rejected =
preparedOperator.TryPrepare({.enthalpy = enthalpy, .displacement = displacement}, dependencies);
REQUIRE_FALSE(rejected.has_value());
CHECK(rejected.error().reason == mean_field::operators::PressureForcePreparationRejectionReason::invalid_mapping);
CHECK(rejected.error().mappingStatus == mean_field::mapping::MappingStatus::non_positive_determinant);
CHECK_FALSE(preparedOperator.IsPrepared());
CHECK_THROWS_AS(
preparedOperator.Prepare({.enthalpy = enthalpy, .displacement = displacement}, dependencies), std::domain_error
);
displacement = 0.0;
++dependencies.displacement.revision;
const auto accepted =
preparedOperator.TryPrepare({.enthalpy = enthalpy, .displacement = displacement}, dependencies);
REQUIRE(accepted.has_value());
CHECK(preparedOperator.IsPrepared());
}
TEST_CASE(
"Prepared Pressure Force Jacobian Matches Full Stateless Columns "
"Through FieldDof Restriction",

View File

@@ -2,6 +2,7 @@
#include <array>
#include <cmath>
#include <limits>
#include <stdexcept>
#include <catch2/catch_test_macros.hpp>
@@ -127,6 +128,26 @@ namespace rotational_displacement_force_test_utils {
return direction;
}
[[nodiscard]] mfem::Vector make_affine_displacement(
const mean_field::fem::FEM &f,
const double scale
) {
mfem::ParGridFunction field(f.displacementFes.get());
mfem::VectorFunctionCoefficient coefficient(
f.mesh->Dimension(), [scale](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(position.Size());
for (int dimension = 0; dimension < position.Size(); ++dimension) {
value(dimension) = scale * position(dimension);
}
}
);
field.ProjectCoefficient(coefficient);
mfem::Vector result;
field.GetTrueDofs(result);
return result;
}
[[nodiscard]] mean_field::physics::RigidRotation make_rotation(const double scale = 1.0) {
mfem::Vector angularVelocity(3);
angularVelocity(0) = scale * 0.17;
@@ -377,6 +398,85 @@ TEST_CASE(
CHECK(rotational_displacement_force_test_utils::global_norm(zeroRotationResidual, f.mesh->GetComm()) == 0.0);
}
TEST_CASE(
"Rotational Displacement Force Reports Candidate Mapping And Arithmetic Rejections",
tags::rotation_prepared_unit
) {
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());
mfem::Vector density = rotational_displacement_force_test_utils::make_density(f, 0.29);
mfem::Vector displacement = rotational_displacement_force_test_utils::make_affine_displacement(f, -2.0);
const mean_field::physics::RigidRotation rotation = rotational_displacement_force_test_utils::make_rotation(0.83);
mfem::Vector residual;
const auto invalidMapping = mean_field::operators::kernels::try_apply_rotational_displacement_force_residual(
f, *f.domainMapperStateless, rotation, density, displacement, residual
);
REQUIRE_FALSE(invalidMapping.has_value());
CHECK(
invalidMapping.error().reason ==
mean_field::operators::kernels::RotationalDisplacementForceRejectionReason::invalid_mapping
);
CHECK(invalidMapping.error().mappingStatus == mean_field::mapping::MappingStatus::non_positive_determinant);
CHECK_THROWS_AS(
mean_field::operators::kernels::apply_rotational_displacement_force_residual(
f, *f.domainMapperStateless, rotation, density, displacement, residual
),
std::domain_error
);
displacement = 0.0;
density = 1.0e200;
const mean_field::physics::RigidRotation extremeRotation =
rotational_displacement_force_test_utils::make_rotation(1.0e100);
const auto nonFiniteArithmetic = mean_field::operators::kernels::try_apply_rotational_displacement_force_residual(
f, *f.domainMapperStateless, extremeRotation, density, displacement, residual
);
REQUIRE_FALSE(nonFiniteArithmetic.has_value());
CHECK(
nonFiniteArithmetic.error().reason ==
mean_field::operators::kernels::RotationalDisplacementForceRejectionReason::non_finite_arithmetic
);
mean_field::operators::PreparedRotationalDisplacementForceOperator preparedOperator(f, *f.domainMapperStateless);
const auto &context = preparedOperator.GetContext();
auto dependencies = rotational_displacement_force_test_utils::make_dependencies();
mfem::Vector reducedDensity = context.GetDensityMap().gather(density);
const mfem::Vector reducedDisplacement = context.GetDisplacementMap().gather(displacement);
const auto preparedRejection = preparedOperator.TryPrepare(
{.density = reducedDensity, .displacement = reducedDisplacement}, dependencies, extremeRotation
);
REQUIRE_FALSE(preparedRejection.has_value());
CHECK(
preparedRejection.error().reason ==
mean_field::operators::kernels::RotationalDisplacementForceRejectionReason::non_finite_arithmetic
);
CHECK_FALSE(preparedOperator.IsPrepared());
CHECK_THROWS_AS(
preparedOperator.Prepare(
{.density = reducedDensity, .displacement = reducedDisplacement}, dependencies, extremeRotation
),
std::domain_error
);
density = rotational_displacement_force_test_utils::make_density(f, 0.29);
reducedDensity = context.GetDensityMap().gather(density);
++dependencies.density.revision;
++dependencies.rotation.revision;
const auto preparedAccepted = preparedOperator.TryPrepare(
{.density = reducedDensity, .displacement = reducedDisplacement}, dependencies, rotation
);
REQUIRE(preparedAccepted.has_value());
CHECK(preparedOperator.IsPrepared());
const auto accepted = mean_field::operators::kernels::try_apply_rotational_displacement_force_residual(
f, *f.domainMapperStateless, rotation, density, displacement, residual
);
CHECK(accepted.has_value());
}
TEST_CASE(
"Prepared Rotational Displacement Force Reprepares Selectively",
tags::rotation_prepared

View File

@@ -1505,6 +1505,69 @@ TEST_CASE(
CHECK(targetReport.assembledResidual);
}
TEST_CASE(
"Prepared Stellar Trial Preparation Reports Folded Geometry Without Unwinding",
tags::reduced_stellar_geometry &tags::prepared &tags::geometry &tags::unit
) {
mean_field::utils::Args args = test_utils::setup_args();
mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.okay());
const mean_field::eos::Polytrope barotrope(3.0, 0.25);
const auto stellarModel = stellar_equilibrium_test_utils::make_stellar_model(barotrope, 1.15);
const auto parameterGeometry = stellarModel.compileDomainDeformation(f);
const auto &surface = parameterGeometry.surfaceDeformationPrescription();
mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator(
f, *f.domainMapperStateless, stellarModel
);
const auto &layout = stellarOperator.GetLayout();
mfem::Vector state = stellar_equilibrium_test_utils::make_state(f, layout);
auto dependencies = stellar_equilibrium_test_utils::make_dependencies();
const auto rotation = stellar_equilibrium_test_utils::make_zero_rotation();
const double finiteStateValue = state(0);
state(0) = std::numeric_limits<double>::quiet_NaN();
const auto nonFiniteState = stellarOperator.TryPrepare(state, dependencies, rotation);
REQUIRE_FALSE(nonFiniteState.has_value());
CHECK(
nonFiniteState.error().reason ==
mean_field::operators::StellarEquilibriumPreparationRejectionReason::non_finite_physics
);
CHECK_FALSE(stellarOperator.IsPrepared());
CHECK_THROWS_AS(stellarOperator.Prepare(state, dependencies, rotation), std::domain_error);
state(0) = finiteStateValue;
mfem::Vector foldingParameters(surface.parameterCount());
for (int parameter = 0; parameter < foldingParameters.Size(); ++parameter) {
foldingParameters(parameter) = -1.5 * surface.referenceRadius(parameter);
}
stellar_equilibrium_test_utils::assign_value_block(
state, layout, stellar_equilibrium_test_utils::displacementValue, foldingParameters
);
const auto rejected = stellarOperator.TryPrepare(state, dependencies, rotation);
REQUIRE_FALSE(rejected.has_value());
CHECK(
rejected.error().reason ==
mean_field::operators::StellarEquilibriumPreparationRejectionReason::inverted_geometry
);
CHECK(rejected.error().stage == mean_field::operators::StellarEquilibriumPreparationStage::generated_geometry);
CHECK(std::isfinite(rejected.error().minimumJacobianDeterminant));
CHECK(rejected.error().minimumJacobianDeterminant <= 0.0);
CHECK_FALSE(stellarOperator.IsPrepared());
CHECK_THROWS_AS(stellarOperator.Prepare(state, dependencies, rotation), std::domain_error);
foldingParameters = 0.0;
stellar_equilibrium_test_utils::assign_value_block(
state, layout, stellar_equilibrium_test_utils::displacementValue, foldingParameters
);
const auto accepted = stellarOperator.TryPrepare(state, dependencies, rotation);
REQUIRE(accepted.has_value());
CHECK(accepted->generatedGeometry.isOrientationPreserving());
CHECK(stellarOperator.IsPrepared());
}
TEST_CASE(
"Accepted Reduced Geometries Remain Valid Across Prepared Stellar Physics Quadrature Rules",
tags::reduced_stellar_geometry &tags::prepared &tags::geometry &tags::self_consistency

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
#include <algorithm>
#include <cmath>
#include <concepts>
#include <limits>
#include <numbers>
#include <type_traits>
#include <utility>
@@ -12,8 +13,7 @@ import mean_field;
import test_helpers;
namespace outer_manifest_report_test {
template <mean_field::model::StellarModelType Model>
class PreparedEarlierMultiplier;
template <mean_field::model::StellarModelType Model> class PreparedEarlierMultiplier;
class EarlierMultiplier final {
public:
@@ -36,12 +36,9 @@ namespace outer_manifest_report_test {
"R_a",
"specific_energy",
"specific_energy">>;
using EquilibriumPhysics =
mean_field::operators::SpecificationEquilibriumPhysics<
PreparedEarlierMultiplier>;
using EquilibriumPhysics = mean_field::operators::SpecificationEquilibriumPhysics<PreparedEarlierMultiplier>;
explicit EarlierMultiplier(const Parameters parameters) noexcept
: m_target(parameters.target) {
explicit EarlierMultiplier(const Parameters parameters) noexcept : m_target(parameters.target) {
}
[[nodiscard]] mean_field::dimensions::SpecificEnergyValue target() const noexcept {
@@ -52,20 +49,20 @@ namespace outer_manifest_report_test {
mean_field::dimensions::SpecificEnergyValue m_target;
};
template <mean_field::model::StellarModelType Model>
class PreparedEarlierMultiplier final {
template <mean_field::model::StellarModelType Model> class PreparedEarlierMultiplier final {
public:
using Report = mean_field::operators::EmptySpecificationPreparationReport;
explicit PreparedEarlierMultiplier(const EarlierMultiplier &) noexcept {
}
template <typename StateView>
[[nodiscard]] Report PrepareAfterPhysical(const StateView &) noexcept {
template <typename StateView> [[nodiscard]] Report PrepareAfterPhysical(const StateView &) noexcept {
return {};
}
template <typename Equation, typename Row>
template <
typename Equation,
typename Row>
[[nodiscard]] mean_field::stellar::StructuralZero AddResidual(
Equation,
Row &
@@ -73,9 +70,15 @@ namespace outer_manifest_report_test {
return mean_field::stellar::structuralZero;
}
template <typename Equation, typename State, typename Direction, typename Row>
template <
typename Equation,
typename State,
typename Direction,
typename Row>
[[nodiscard]] mean_field::stellar::StructuralZero AddJacobianAction(
mean_field::stellar::Derivative<Equation, State>,
mean_field::stellar::Derivative<
Equation,
State>,
const Direction &,
Row &
) const noexcept {
@@ -89,40 +92,38 @@ namespace outer_manifest_report_test {
} // namespace outer_manifest_report_test
namespace {
using BaseModel = mean_field::model::StellarModel<mean_field::models::SpecificationSet<
using BaseModel = mean_field::model::StellarModel<mean_field::models::SpecificationSet<
mean_field::eos::Polytrope,
mean_field::surface::Isobaric,
mean_field::integral::FixedTotalMass>>;
using CentralDensityModel = mean_field::model::StellarModel<mean_field::models::SpecificationSet<
using CentralDensityModel = mean_field::model::StellarModel<mean_field::models::SpecificationSet<
mean_field::eos::Polytrope,
mean_field::surface::Isobaric,
mean_field::integral::FixedTotalMass,
mean_field::constraint::FixedCentralDensity>>;
using AngularMomentumModel = mean_field::model::StellarModel<mean_field::models::SpecificationSet<
using AngularMomentumModel = mean_field::model::StellarModel<mean_field::models::SpecificationSet<
mean_field::eos::Polytrope,
mean_field::surface::Isobaric,
mean_field::integral::FixedTotalMass,
mean_field::integral::FixedAngularMomentum>>;
using AngularMomentumCentralDensityModel =
mean_field::model::StellarModel<mean_field::models::SpecificationSet<
mean_field::eos::Polytrope,
mean_field::surface::Isobaric,
mean_field::integral::FixedTotalMass,
mean_field::integral::FixedAngularMomentum,
mean_field::constraint::FixedCentralDensity>>;
using AngularMomentumCentralDensityModel = mean_field::model::StellarModel<mean_field::models::SpecificationSet<
mean_field::eos::Polytrope,
mean_field::surface::Isobaric,
mean_field::integral::FixedTotalMass,
mean_field::integral::FixedAngularMomentum,
mean_field::constraint::FixedCentralDensity>>;
using IncompleteModel =
mean_field::model::StellarModel<mean_field::models::SpecificationSet<mean_field::eos::Polytrope>>;
using EarlierMultiplierModel =
mean_field::model::StellarModel<mean_field::models::SpecificationSet<
mean_field::eos::Polytrope,
mean_field::surface::Isobaric,
outer_manifest_report_test::EarlierMultiplier,
mean_field::integral::FixedTotalMass>>;
using EarlierMultiplierModel = mean_field::model::StellarModel<mean_field::models::SpecificationSet<
mean_field::eos::Polytrope,
mean_field::surface::Isobaric,
outer_manifest_report_test::EarlierMultiplier,
mean_field::integral::FixedTotalMass>>;
template <typename Candidate>
concept HasLegacyNumericalModelAdapter = requires { typename Candidate::NumericalModelAdapter; };
@@ -166,8 +167,8 @@ TEST_CASE(
) {
using namespace mean_field;
using BaseProblem = equilibrium::StellarEquilibriumProblem<BaseModel>;
using CentralDensityProblem = equilibrium::StellarEquilibriumProblem<CentralDensityModel>;
using BaseProblem = equilibrium::StellarEquilibriumProblem<BaseModel>;
using CentralDensityProblem = equilibrium::StellarEquilibriumProblem<CentralDensityModel>;
using AngularMomentumProblem = equilibrium::StellarEquilibriumProblem<AngularMomentumModel>;
using AngularMomentumCentralDensityProblem =
equilibrium::StellarEquilibriumProblem<AngularMomentumCentralDensityModel>;
@@ -178,14 +179,17 @@ TEST_CASE(
STATIC_CHECK(equilibrium::StellarEquilibriumModel<AngularMomentumCentralDensityModel>);
STATIC_CHECK(equilibrium::StellarEquilibriumModel<EarlierMultiplierModel>);
STATIC_CHECK_FALSE(equilibrium::StellarEquilibriumModel<IncompleteModel>);
STATIC_CHECK_FALSE(operators::StellarEquilibriumRuntimeContribution<
outer_manifest_report_test::EarlierMultiplier>::registered);
STATIC_CHECK_FALSE(operators::stellarEquilibriumBackendRuntimeAuthorized<
outer_manifest_report_test::EarlierMultiplier,
EarlierMultiplierModel>);
STATIC_CHECK(operators::StellarEquilibriumPhysicsAvailableFor<
outer_manifest_report_test::EarlierMultiplier,
EarlierMultiplierModel>);
STATIC_CHECK_FALSE(
operators::StellarEquilibriumRuntimeContribution<outer_manifest_report_test::EarlierMultiplier>::registered
);
STATIC_CHECK_FALSE(
operators::stellarEquilibriumBackendRuntimeAuthorized<
outer_manifest_report_test::EarlierMultiplier, EarlierMultiplierModel>
);
STATIC_CHECK(
operators::StellarEquilibriumPhysicsAvailableFor<
outer_manifest_report_test::EarlierMultiplier, EarlierMultiplierModel>
);
STATIC_CHECK_FALSE(std::same_as<BaseProblem, CentralDensityProblem>);
STATIC_CHECK(BaseProblem::symbolicallySquare);
STATIC_CHECK(CentralDensityProblem::symbolicallySquare);
@@ -213,20 +217,23 @@ TEST_CASE(
typename AngularMomentumProblem::PreparedOperatorType,
operators::PreparedVariadicStellarEquilibriumOperator<AngularMomentumModel>>
);
STATIC_CHECK_FALSE(std::same_as<
typename BaseProblem::PreparedOperatorType,
typename CentralDensityProblem::PreparedOperatorType>);
STATIC_CHECK_FALSE(std::same_as<
typename AngularMomentumProblem::PreparedOperatorType,
typename AngularMomentumCentralDensityProblem::PreparedOperatorType>);
STATIC_CHECK_FALSE(
std::same_as<typename BaseProblem::PreparedOperatorType, typename CentralDensityProblem::PreparedOperatorType>
);
STATIC_CHECK_FALSE(
std::same_as<
typename AngularMomentumProblem::PreparedOperatorType,
typename AngularMomentumCentralDensityProblem::PreparedOperatorType>
);
STATIC_CHECK(AngularMomentumProblem::FormType::value_block_count == 7);
STATIC_CHECK(AngularMomentumCentralDensityProblem::FormType::value_block_count == 8);
STATIC_CHECK(std::same_as<
typename BaseProblem::FormType,
utils::blocks::surface_deformed_stellar_equilibrium_form>);
STATIC_CHECK(std::same_as<
typename CentralDensityProblem::FormType,
utils::blocks::central_density_bordered_stellar_equilibrium_form>);
STATIC_CHECK(
std::same_as<typename BaseProblem::FormType, utils::blocks::surface_deformed_stellar_equilibrium_form>
);
STATIC_CHECK(
std::same_as<
typename CentralDensityProblem::FormType, utils::blocks::central_density_bordered_stellar_equilibrium_form>
);
STATIC_CHECK(
std::same_as<
typename BaseProblem::CompiledSurfaceConstraintType,
@@ -242,34 +249,29 @@ TEST_CASE(
) {
using namespace mean_field;
utils::Args arguments = test_utils::setup_args();
utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
constexpr double radius = utils::RADIUS;
constexpr double mass = utils::MASS;
constexpr double radius = utils::RADIUS;
constexpr double mass = utils::MASS;
constexpr double targetAngularMomentum = 0.1;
const double polytropicConstant = 2.0 * utils::G * radius * radius / std::numbers::pi_v<double>;
const double seedCentralDensity =
std::numbers::pi_v<double> * mass / (4.0 * radius * radius * radius);
auto model = model::StellarModel(
const double polytropicConstant = 2.0 * utils::G * radius * radius / std::numbers::pi_v<double>;
const double seedCentralDensity = std::numbers::pi_v<double> * mass / (4.0 * radius * radius * radius);
auto model = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}),
integral::FixedAngularMomentum({
.Jtotal = dimensions::AngularMomentumValue{targetAngularMomentum},
.axis = {0.0, 0.0, 3.0}
})
integral::FixedAngularMomentum(
{.Jtotal = dimensions::AngularMomentumValue{targetAngularMomentum}, .axis = {0.0, 0.0, 3.0}}
)
);
auto problem = equilibrium::discretize(model, finiteElements);
auto problem = equilibrium::discretize(model, std::move(finiteElements));
auto projected = seed::makeProjectedEquilibriumState(
problem,
seed::LaneEmden({
.centralDensity = dimensions::DensityValue{seedCentralDensity},
.radialSampleCount = 1024
})
seed::LaneEmden({.centralDensity = dimensions::DensityValue{seedCentralDensity}, .radialSampleCount = 1024})
);
auto dependencies = make_dependencies();
auto dependencies = make_dependencies();
const auto preparation = problem.Prepare(projected.values, dependencies);
CHECK(preparation.generatedPhysicalControl);
@@ -284,7 +286,7 @@ TEST_CASE(
CHECK(std::abs(angularReport.scaledResidual) < 7.0e-4);
mfem::Vector direction(problem.StateSize());
direction = 0.0;
direction = 0.0;
mfem::Vector angularVelocityDirection = problem.GetManifest().stateView(direction).block(
utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term
);
@@ -309,15 +311,15 @@ TEST_CASE(
plusResidual -= minusResidual;
plusResidual /= 2.0 * step;
auto analyticView = problem.GetManifest().residualView(analyticAction);
auto differenceView = problem.GetManifest().residualView(plusResidual);
auto analyticView = problem.GetManifest().residualView(analyticAction);
auto differenceView = problem.GetManifest().residualView(plusResidual);
const auto blockError = [&](const auto &term) {
const mfem::Vector analytic = analyticView.block(term);
const mfem::Vector analytic = analyticView.block(term);
const mfem::Vector difference = differenceView.block(term);
return relative_difference(analytic, difference);
};
const double surfaceError = blockError(utils::blocks::surface_deformation_field.shape_equilibrium_term);
const double surfaceError = blockError(utils::blocks::surface_deformation_field.shape_equilibrium_term);
const double enthalpyError = blockError(utils::blocks::enthalpy_field.specific_term);
const double angularMomentumError =
blockError(utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term);
@@ -335,13 +337,48 @@ TEST_CASE(
CHECK(analyticView.block(utils::blocks::density_field.mass_term).Norml2() == 0.0);
CHECK(analyticView.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term).Norml2() == 0.0);
mfem::Vector nonFiniteAngularVelocityState(projected.values);
auto nonFiniteAngularVelocity = problem.GetManifest()
.stateView(nonFiniteAngularVelocityState)
.block(utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term);
REQUIRE(nonFiniteAngularVelocity.Size() == 1);
nonFiniteAngularVelocity(0) = std::numeric_limits<double>::quiet_NaN();
nonFiniteAngularVelocity.SyncAliasMemory(nonFiniteAngularVelocityState);
const auto nonFiniteControl = problem.TryPrepare(nonFiniteAngularVelocityState, dependencies);
REQUIRE_FALSE(nonFiniteControl.has_value());
CHECK(
nonFiniteControl.error().reason == operators::StellarEquilibriumPreparationRejectionReason::non_finite_physics
);
CHECK_FALSE(problem.IsPrepared());
REQUIRE(problem.TryPrepare(projected.values, dependencies).has_value());
mfem::Vector negativeDensityState(projected.values);
auto negativeDensity =
problem.GetManifest().stateView(negativeDensityState).block(utils::blocks::density_field.mass_term);
negativeDensity *= -1.0;
negativeDensity.SyncAliasMemory(negativeDensityState);
auto negativeDensityDependencies = dependencies;
++negativeDensityDependencies.density.revision;
const auto inadmissibleMoment = problem.TryPrepare(negativeDensityState, negativeDensityDependencies);
REQUIRE_FALSE(inadmissibleMoment.has_value());
CHECK(
inadmissibleMoment.error().reason ==
operators::StellarEquilibriumPreparationRejectionReason::inadmissible_physics
);
CHECK_FALSE(problem.IsPrepared());
++negativeDensityDependencies.density.revision;
REQUIRE(problem.TryPrepare(projected.values, negativeDensityDependencies).has_value());
CHECK(problem.IsPrepared());
auto zeroModel = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}),
integral::FixedAngularMomentum({.Jtotal = dimensions::AngularMomentumValue{0.0}})
);
auto zeroProblem = equilibrium::discretize(zeroModel, finiteElements);
fem::FEM zeroFiniteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(zeroFiniteElements.okay());
auto zeroProblem = equilibrium::discretize(zeroModel, std::move(zeroFiniteElements));
mfem::Vector zeroState(projected.values);
zeroProblem.GetManifest().stateView(zeroState).block(
utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term
@@ -361,47 +398,37 @@ TEST_CASE(
) {
using namespace mean_field;
using Form = operators::CompiledStellarEquilibriumForm<EarlierMultiplierModel>;
using EarlierValue = utils::blocks::generated_value_block<
models::MultiplierFor<outer_manifest_report_test::EarlierMultiplier>>;
using EarlierValue =
utils::blocks::generated_value_block<models::MultiplierFor<outer_manifest_report_test::EarlierMultiplier>>;
using MassValue = utils::blocks::fixed_total_mass::mass_normalization::value;
STATIC_CHECK(utils::blocks::type_index_v<
EarlierValue,
typename Form::value_blocks> == 5);
STATIC_CHECK(utils::blocks::type_index_v<
MassValue,
typename Form::value_blocks> == 6);
STATIC_CHECK(utils::blocks::type_index_v<EarlierValue, typename Form::value_blocks> == 5);
STATIC_CHECK(utils::blocks::type_index_v<MassValue, typename Form::value_blocks> == 6);
utils::Args arguments = test_utils::setup_args();
utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
auto model = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = 0.25}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
outer_manifest_report_test::EarlierMultiplier({
.target = dimensions::SpecificEnergyValue{0.75}}),
eos::Polytrope({.n = 1.0, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
outer_manifest_report_test::EarlierMultiplier({.target = dimensions::SpecificEnergyValue{0.75}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.25}})
);
auto problem = equilibrium::discretize(model, finiteElements);
auto problem = equilibrium::discretize(model, std::move(finiteElements));
mfem::Vector state(problem.StateSize());
state = 0.0;
const auto stateView = problem.GetManifest().stateView(state);
stateView.block(utils::blocks::density_field.mass_term) = 1.0;
state = 0.0;
const auto stateView = problem.GetManifest().stateView(state);
stateView.block(utils::blocks::density_field.mass_term) = 1.0;
stateView.block(utils::blocks::enthalpy_field.specific_term) = 1.0;
stateView.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term) = 0.25;
const auto preparation = problem.Prepare(
state,
make_dependencies(),
make_zero_rotation()
);
REQUIRE(preparation.physical.DidAnyWork());
const auto preparation = problem.TryPrepare(state, make_dependencies(), make_zero_rotation());
REQUIRE(preparation.has_value());
REQUIRE(preparation->physical.DidAnyWork());
const auto report = problem.GetPreparedOperator().GetFixedMassReport();
const auto &outerDescriptor =
problem.GetManifest().template specification<models::FixedTotalMass>();
const auto report = problem.GetPreparedOperator().GetFixedMassReport();
const auto &outerDescriptor = problem.GetManifest().template specification<models::FixedTotalMass>();
CHECK(report.descriptor.stableId == outerDescriptor.stableId);
CHECK(report.descriptor.valueBlock == outerDescriptor.valueBlock);
CHECK(report.descriptor.residualBlock == outerDescriptor.residualBlock);
@@ -409,8 +436,7 @@ TEST_CASE(
CHECK(report.descriptor.residualBlock == 6);
CHECK(report.descriptor.target == 1.25);
CHECK(report.dimensionalResidual == report.achieved - report.descriptor.target);
CHECK(report.scaledResidual ==
report.dimensionalResidual / report.descriptor.residualScale);
CHECK(report.scaledResidual == report.dimensionalResidual / report.descriptor.residualScale);
}
TEST_CASE(
@@ -419,32 +445,38 @@ TEST_CASE(
) {
using namespace mean_field;
utils::Args args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.okay());
utils::Args args = test_utils::setup_args();
fem::FEM legacyFiniteElements = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(legacyFiniteElements.okay());
fem::FEM modelDrivenFiniteElements = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(modelDrivenFiniteElements.okay());
const MPI_Comm modelDrivenCommunicator = modelDrivenFiniteElements.mesh->GetComm();
const mapping::DomainMapper *modelDrivenMapper = modelDrivenFiniteElements.domainMapperStateless.get();
models::StellarModel legacyModel{
models::structure::PolytropicStructure{eos::Polytrope{3.0, 0.25}, 1.25},
surface::ConstantPressureSurface{eos::PressureValue{0.0}}
};
operators::PreparedStellarEquilibriumOperator legacyOperator(f, *f.domainMapperStateless, legacyModel);
operators::PreparedStellarEquilibriumOperator legacyOperator(
legacyFiniteElements, *legacyFiniteElements.domainMapperStateless, legacyModel
);
const equilibrium::StellarDiscretization discretization{f, *f.domainMapperStateless};
auto equilibriumProblem = equilibrium::discretize(
model::StellarModel(
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.25}}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}), eos::Polytrope({.n = 3.0, .K = 0.25})
),
discretization
std::move(modelDrivenFiniteElements)
);
auto &modelDrivenOperator = equilibriumProblem.GetPreparedOperator();
auto &modelDrivenOperator = equilibriumProblem.GetPreparedOperator();
const auto &physicalOperator = equilibriumProblem.GetPhysicalOperator();
CHECK(equilibriumProblem.StateSize() == legacyOperator.Width());
CHECK(equilibriumProblem.EquationSize() == legacyOperator.Height());
CHECK(equilibriumProblem.StateSize() == equilibriumProblem.EquationSize());
CHECK(&equilibriumProblem.GetDiscretization().finiteElementModel() == &f);
CHECK(&equilibriumProblem.GetDiscretization().domainMapper() == f.domainMapperStateless.get());
CHECK(equilibriumProblem.GetCommunicator() == modelDrivenCommunicator);
CHECK(&equilibriumProblem.GetDiscretization().domainMapper() == modelDrivenMapper);
CHECK(equilibriumProblem.GetDiscretization().isCurrent());
CHECK(physicalOperator.GetTargetMass() == 1.25);
CHECK(physicalOperator.GetSurfaceConstraintOperator().GetPhysicalCondition().targetPressure == 0.0);
@@ -455,7 +487,7 @@ TEST_CASE(
mfem::Vector state(legacyOperator.Width());
state = 0.0;
const auto stateView = legacyOperator.GetRootManifest().stateView(state);
const auto stateView = legacyOperator.GetRootManifest().stateView(state);
stateView.block(utils::blocks::density_field.mass_term) = 1.0;
stateView.block(utils::blocks::enthalpy_field.specific_term) = 1.0;
@@ -487,29 +519,28 @@ TEST_CASE(
) {
using namespace mean_field;
utils::Args arguments = test_utils::setup_args();
utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
auto model = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = 0.25}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
eos::Polytrope({.n = 1.0, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}}),
integral::FixedAngularMomentum({.Jtotal = dimensions::AngularMomentumValue{0.2}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{1.0}})
);
auto problem = equilibrium::discretize(model, finiteElements);
auto problem = equilibrium::discretize(model, std::move(finiteElements));
using Problem = std::remove_cvref_t<decltype(problem)>;
STATIC_CHECK(Problem::FormType::value_block_count == 8);
STATIC_CHECK(Problem::FormType::residual_block_count == 8);
mfem::Vector state(problem.StateSize());
state = 0.0;
const auto stateView = problem.GetManifest().stateView(state);
stateView.block(utils::blocks::density_field.mass_term) = 1.0;
state = 0.0;
const auto stateView = problem.GetManifest().stateView(state);
stateView.block(utils::blocks::density_field.mass_term) = 1.0;
stateView.block(utils::blocks::enthalpy_field.specific_term) = 1.0;
stateView.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term) = 0.25;
stateView.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term) = 0.25;
stateView.block(utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term) = 0.4;
stateView.block(utils::blocks::fixed_central_density_phase.central_value_term) = 0.03;
stateView.block(utils::blocks::fixed_central_density_phase.central_value_term) = 0.03;
const auto report = problem.Prepare(state, make_dependencies());
CHECK(report.template specification<models::FixedCentralDensity>().constraint.DidAnyWork());
@@ -517,18 +548,18 @@ TEST_CASE(
CHECK(problem.IsPrepared());
CHECK(problem.StateSize() == problem.GetPhysicalOperator().Width() + 2);
REQUIRE(problem.GetManifest().constraints().size() == 4);
CHECK(problem.GetManifest().template specification<models::FixedAngularMomentum>().stableId ==
"FixedAngularMomentum");
CHECK(problem.GetManifest().template specification<models::FixedCentralDensity>().stableId ==
"FixedCentralDensity");
CHECK(
problem.GetManifest().template specification<models::FixedAngularMomentum>().stableId == "FixedAngularMomentum"
);
CHECK(
problem.GetManifest().template specification<models::FixedCentralDensity>().stableId == "FixedCentralDensity"
);
mfem::Vector residual;
problem.BuildResidual(residual);
REQUIRE(residual.Size() == problem.EquationSize());
const auto residualView = problem.GetManifest().residualView(residual);
CHECK(std::isfinite(
residualView.block(utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term)(0)
));
CHECK(std::isfinite(residualView.block(utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term)(0)));
CHECK(std::isfinite(residualView.block(utils::blocks::fixed_central_density_phase.central_value_term)(0)));
mfem::Vector direction(problem.StateSize());