feat(surface): major work on implementing surface constraints in a presciption agnostic manner

This commit is contained in:
2026-08-30 16:41:14 -04:00
parent 36adfa1174
commit 0a7f18c5c7
95 changed files with 30144 additions and 25766 deletions

View File

@@ -94,6 +94,65 @@ namespace field_dof_map_test_utils {
domain::RelationList<>>;
} // namespace field_dof_map_test_utils
TEST_CASE(
"Field Boundary DOF Map Selects The Stellar Surface In Reduced Field Ordering",
tags::surface_boundary_dof_topology
) {
namespace domain = mean_field::utils::domain;
namespace field = mean_field::field;
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 field::FieldDofMap enthalpyMap =
field::make_field_dof_map<field::Enthalpy, field_dof_map_test_utils::Schema>(*f.enthalpyFes);
const field::FieldBoundaryDofMap stellarSurface =
field::make_field_boundary_dof_map<field::Enthalpy, domain::StellarSurface, field_dof_map_test_utils::Schema>(
*f.enthalpyFes, enthalpyMap
);
CHECK(stellarSurface.field_size() == enthalpyMap.reduced_size());
CHECK(field_dof_map_test_utils::global_sum(stellarSurface.size()) > 0);
CHECK(
field_dof_map_test_utils::global_sum(stellarSurface.size()) <
field_dof_map_test_utils::global_sum(enthalpyMap.reduced_size())
);
for (const int reducedDof : stellarSurface.reduced_dofs()) {
CAPTURE(reducedDof);
CHECK(stellarSurface.contains(reducedDof));
CHECK(enthalpyMap.contains_true_dof(enthalpyMap.true_dof(reducedDof)));
}
}
TEST_CASE(
"Field Point DOF Map Selects One Vector Vertex At The Computational Origin",
tags::translational_centering_topology
) {
namespace field = mean_field::field;
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 field::FieldDofMap displacementMap =
field::make_field_dof_map<field::Displacement, field_dof_map_test_utils::Schema>(*f.displacementFes);
mfem::Vector origin(f.mesh->SpaceDimension());
origin = 0.0;
const field::FieldPointDofMap centerRows =
field::make_field_point_dof_map<field::Displacement>(*f.displacementFes, displacementMap, origin, 1.0e-12);
CHECK(centerRows.field_size() == displacementMap.reduced_size());
CHECK(field_dof_map_test_utils::global_sum(centerRows.size()) == f.mesh->SpaceDimension());
for (const int reducedDof : centerRows.reduced_dofs()) {
CAPTURE(reducedDof);
CHECK(centerRows.contains(reducedDof));
CHECK(displacementMap.contains_true_dof(displacementMap.true_dof(reducedDof)));
}
}
TEST_CASE(
"Field DOF Map Preserves Canonical Bidirectional Indexing",
tags::field_dof_unit
@@ -523,9 +582,7 @@ TEST_CASE(
STATIC_REQUIRE(field_dof_map_test_utils::CanMakeFieldDofGridFunctionAdapter<field::Displacement>);
STATIC_REQUIRE_FALSE(
field_dof_map_test_utils::CanMakeFieldDofGridFunctionAdapter<field::BarotropicConstant>
);
STATIC_REQUIRE_FALSE(field_dof_map_test_utils::CanMakeFieldDofGridFunctionAdapter<field::BarotropicConstant>);
CHECK(true);
}
@@ -750,9 +807,8 @@ TEST_CASE(
mfem::Mesh serialMesh = field_dof_map_test_utils::make_split_mesh();
mfem::ParMesh mesh(MPI_COMM_WORLD, serialMesh);
auto fec = field::Field<field::Density>::make_fec<field::Density::Scalar>(2);
auto finiteElementSpace =
field::Field<field::Density>::make_fespace<field::Density::Scalar>(mesh, *fec);
auto fec = field::Field<field::Density>::make_fec<field::Density::Scalar>(2);
auto finiteElementSpace = field::Field<field::Density>::make_fespace<field::Density::Scalar>(mesh, *fec);
REQUIRE(finiteElementSpace != nullptr);
@@ -796,9 +852,8 @@ TEST_CASE(
mfem::Mesh serialMesh = field_dof_map_test_utils::make_split_mesh();
mfem::ParMesh mesh(MPI_COMM_WORLD, serialMesh);
auto fec = field::Field<field::Enthalpy>::make_fec<field::Enthalpy::Scalar>(2);
auto finiteElementSpace =
field::Field<field::Enthalpy>::make_fespace<field::Enthalpy::Scalar>(mesh, *fec);
auto fec = field::Field<field::Enthalpy>::make_fec<field::Enthalpy::Scalar>(2);
auto finiteElementSpace = field::Field<field::Enthalpy>::make_fespace<field::Enthalpy::Scalar>(mesh, *fec);
REQUIRE(finiteElementSpace != nullptr);
@@ -849,9 +904,8 @@ TEST_CASE(
mfem::Mesh serialMesh = field_dof_map_test_utils::make_split_mesh();
mfem::ParMesh mesh(MPI_COMM_WORLD, serialMesh);
auto fec = field::Field<field::Density>::make_fec<field::Density::Scalar>(2);
auto finiteElementSpace =
field::Field<field::Density>::make_fespace<field::Density::Scalar>(mesh, *fec);
auto fec = field::Field<field::Density>::make_fec<field::Density::Scalar>(2);
auto finiteElementSpace = field::Field<field::Density>::make_fespace<field::Density::Scalar>(mesh, *fec);
REQUIRE(finiteElementSpace != nullptr);
@@ -900,9 +954,8 @@ TEST_CASE(
mfem::Mesh serialMesh = field_dof_map_test_utils::make_split_mesh();
mfem::ParMesh mesh(MPI_COMM_WORLD, serialMesh);
auto fec = field::Field<field::Displacement>::make_fec<field::Displacement::Vector>(2);
auto finiteElementSpace =
field::Field<field::Displacement>::make_fespace<field::Displacement::Vector>(mesh, *fec);
auto fec = field::Field<field::Displacement>::make_fec<field::Displacement::Vector>(2);
auto finiteElementSpace = field::Field<field::Displacement>::make_fespace<field::Displacement::Vector>(mesh, *fec);
REQUIRE(finiteElementSpace != nullptr);
@@ -939,13 +992,11 @@ TEST_CASE(
mfem::Mesh serialMesh = field_dof_map_test_utils::make_split_mesh();
mfem::ParMesh mesh(MPI_COMM_WORLD, serialMesh);
auto fec = field::Field<field::Density>::make_fec<field::Density::Scalar>(2);
auto finiteElementSpace =
field::Field<field::Density>::make_fespace<field::Density::Scalar>(mesh, *fec);
auto fec = field::Field<field::Density>::make_fec<field::Density::Scalar>(2);
auto finiteElementSpace = field::Field<field::Density>::make_fespace<field::Density::Scalar>(mesh, *fec);
auto otherFec = field::Field<field::Density>::make_fec<field::Density::Scalar>(2);
auto otherFiniteElementSpace =
field::Field<field::Density>::make_fespace<field::Density::Scalar>(mesh, *otherFec);
auto otherFec = field::Field<field::Density>::make_fec<field::Density::Scalar>(2);
auto otherFiniteElementSpace = field::Field<field::Density>::make_fespace<field::Density::Scalar>(mesh, *otherFec);
REQUIRE(finiteElementSpace != nullptr);
REQUIRE(otherFiniteElementSpace != nullptr);
@@ -959,8 +1010,7 @@ TEST_CASE(
const mfem::Array<int> empty;
CHECK_THROWS_AS(
(field::FieldDofGridFunctionAdapter(
field::FieldDofMap(finiteElementSpace->GetTrueVSize() + 1, empty),
*finiteElementSpace
field::FieldDofMap(finiteElementSpace->GetTrueVSize() + 1, empty), *finiteElementSpace
)),
std::invalid_argument
);

View File

@@ -11,7 +11,10 @@ using namespace mean_field;
namespace {
struct SerialMappingData {
explicit SerialMappingData(mfem::Mesh &mesh)
: compactification_fes(&mesh, &compactification_fec),
: compactification_fes(
&mesh,
&compactification_fec
),
compactification_coordinate(&compactification_fes),
mapper(field_dof_test_utils::make_domain_mapper()) {
compactification_coordinate = 0.0;
@@ -83,7 +86,7 @@ TEST_CASE(
quadrature::RuleFactory quadrature_factory(std::move(policy));
const quadrature::MappingKind mapping_kind = quadrature::MappingKind::general;
const int position_order = displacement_element->GetOrder();
const int position_order = displacement_element->GetOrder();
quadrature_factory.configure_centrifugal(
integrator, quadrature::QuadratureRole::discretization, *density_element, *velocity_element, *transformation,
@@ -183,7 +186,7 @@ TEST_CASE(
quadrature::RuleFactory quadrature_factory(std::move(policy));
const quadrature::MappingKind mapping_kind = quadrature::MappingKind::general;
const int position_order = displacement_element->GetOrder();
const int position_order = displacement_element->GetOrder();
quadrature_factory.configure_centrifugal(
integrator, quadrature::QuadratureRole::discretization, *density_element, *velocity_element, *transformation,
@@ -344,7 +347,7 @@ TEST_CASE(
quadrature::RuleFactory quadrature_factory(std::move(policy));
const quadrature::MappingKind mapping_kind = quadrature::MappingKind::general;
const int position_order = displacement_element->GetOrder();
const int position_order = displacement_element->GetOrder();
quadrature_factory.configure_centrifugal(
integrator, quadrature::QuadratureRole::discretization, *density_element, *velocity_element, *transformation,
@@ -606,8 +609,8 @@ TEST_CASE(
mapping_evaluator.GetQuadratureContext(*transformation, integration_point);
const double signed_map_determinant = context.detJ;
local_minimum_map_determinant = std::min(local_minimum_map_determinant, signed_map_determinant);
local_maximum_map_determinant = std::max(local_maximum_map_determinant, signed_map_determinant);
local_minimum_map_determinant = std::min(local_minimum_map_determinant, signed_map_determinant);
local_maximum_map_determinant = std::max(local_maximum_map_determinant, signed_map_determinant);
mapping_evaluator.GetPhysicalPoint(*transformation, integration_point, x_physical);
velocity_element->CalcShape(integration_point, velocity_shape);
@@ -808,7 +811,7 @@ TEST_CASE(
mapping_evaluator.GetQuadratureContext(*transformation, integration_point);
const double signed_map_determinant = context.detJ;
local_minimum_determinant = std::min(local_minimum_determinant, signed_map_determinant);
local_minimum_determinant = std::min(local_minimum_determinant, signed_map_determinant);
mapping_evaluator.GetPhysicalPoint(*transformation, integration_point, x_physical);
velocity_element->CalcShape(integration_point, velocity_shape);
@@ -999,7 +1002,7 @@ TEST_CASE(
mapping_evaluator.GetQuadratureContext(*transformation, integration_point);
const double signed_map_determinant = context.detJ;
local_minimum_determinant = std::min(local_minimum_determinant, signed_map_determinant);
local_minimum_determinant = std::min(local_minimum_determinant, signed_map_determinant);
mapping_evaluator.GetPhysicalPoint(*transformation, integration_point, x_physical);
velocity_element->CalcShape(integration_point, velocity_shape);

View File

@@ -43,8 +43,8 @@ TEST_CASE(
mfem::GridFunction displacement(&displacement_fes);
displacement = 0.0;
mfem::GridFunction compactification_coordinate(&compactification_fes);
compactification_coordinate = 0.0;
mapping::DomainMapper domain_mapper = field_dof_test_utils::make_domain_mapper();
compactification_coordinate = 0.0;
mapping::DomainMapper domain_mapper = field_dof_test_utils::make_domain_mapper();
const mfem::FiniteElement *velocity_element = velocity_fes.GetFE(0);
const mfem::FiniteElement *density_element = density_fes.GetFE(0);
@@ -129,8 +129,7 @@ TEST_CASE(
element_residual[displacement_block] = &displacement_residual;
integrators::GravityMomentumIntegrator integrator(
domain_mapper, displacement, compactification_coordinate,
integrators::GravityForceJacobianMode::field_coupled
domain_mapper, displacement, compactification_coordinate, integrators::GravityForceJacobianMode::field_coupled
);
const int maximum_order = std::max(
@@ -297,12 +296,10 @@ TEST_CASE(
mfem::GridFunction displacement(&displacement_fes);
displacement = 0.0;
mfem::GridFunction compactification_coordinate(&compactification_fes);
compactification_coordinate = 0.0;
compactification_coordinate = 0.0;
mapping::DomainMapper domain_mapper = field_dof_test_utils::make_domain_mapper();
mapping::GridFunctionMappingEvaluator mapping_evaluator(
domain_mapper, displacement, compactification_coordinate
);
mapping::GridFunctionMappingEvaluator mapping_evaluator(domain_mapper, displacement, compactification_coordinate);
auto reference_density = [](const mfem::Vector &x) { return 1.0 + x(0); };
@@ -381,8 +378,7 @@ TEST_CASE(
element_residual[displacement_block] = &displacement_residual;
integrators::GravityMomentumIntegrator integrator(
domain_mapper, displacement, compactification_coordinate,
integrators::GravityForceJacobianMode::field_coupled
domain_mapper, displacement, compactification_coordinate, integrators::GravityForceJacobianMode::field_coupled
);
const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get(velocity_element->GetGeomType(), 8);
@@ -471,11 +467,9 @@ TEST_CASE(
mfem::GridFunction displacement(&displacement_fes);
displacement = 0.0;
mfem::GridFunction compactification_coordinate(&compactification_fes);
compactification_coordinate = 0.0;
compactification_coordinate = 0.0;
mapping::DomainMapper domain_mapper = field_dof_test_utils::make_domain_mapper();
mapping::GridFunctionMappingEvaluator mapping_evaluator(
domain_mapper, displacement, compactification_coordinate
);
mapping::GridFunctionMappingEvaluator mapping_evaluator(domain_mapper, displacement, compactification_coordinate);
auto radial_gravity = [](const mfem::Vector &x, mfem::Vector &gravity) {
gravity.SetSize(3);
@@ -553,8 +547,7 @@ TEST_CASE(
element_residual[displacement_block] = &displacement_residual;
integrators::GravityMomentumIntegrator integrator(
domain_mapper, displacement, compactification_coordinate,
integrators::GravityForceJacobianMode::field_coupled
domain_mapper, displacement, compactification_coordinate, integrators::GravityForceJacobianMode::field_coupled
);
const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get(velocity_element->GetGeomType(), 8);

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,6 @@
#include <array>
#include <cmath>
#include <concepts>
#include <limits>
#include <memory>
#include <type_traits>
@@ -12,17 +14,11 @@ import test_helpers;
namespace {
struct StellarModelExtensionTracker final {
int structureValidationCount{0};
int surfaceValidationCount{0};
int surfaceResolutionCount{0};
const mean_field::eos::EquationOfState *structureEquationOfState{nullptr};
const mean_field::eos::EquationOfState *surfaceValidationEquationOfState{nullptr};
const mean_field::eos::EquationOfState *surfaceResolutionEquationOfState{nullptr};
const mean_field::eos::Polytrope *structureEquationOfState{nullptr};
};
class StellarModelTestStructure final : public mean_field::models::structure::StructureBase {
class StellarModelTestStructure final {
public:
explicit StellarModelTestStructure(std::shared_ptr<StellarModelExtensionTracker> tracker)
: m_tracker(std::move(tracker)),
@@ -32,17 +28,17 @@ namespace {
) {
}
[[nodiscard]] const mean_field::eos::EquationOfState &equationOfState() const noexcept override {
[[nodiscard]] const mean_field::eos::Polytrope &equationOfState() const noexcept {
m_tracker->structureEquationOfState = &m_equationOfState;
return m_equationOfState;
}
[[nodiscard]] double targetMass() const noexcept override {
[[nodiscard]] double targetMass() const noexcept {
return 2.5;
}
[[nodiscard]] mean_field::models::structure::StructureSeed
makeInitialSeed(const mean_field::models::structure::StructureSeedRequest &request) const override {
makeInitialSeed(const mean_field::models::structure::StructureSeedRequest &request) const {
mean_field::models::structure::StructureSeed seed;
seed.radius.SetSize(2);
@@ -65,7 +61,7 @@ namespace {
return seed;
}
void validate() const override {
void validate() const {
++m_tracker->structureValidationCount;
}
@@ -74,59 +70,85 @@ namespace {
mean_field::eos::Polytrope m_equationOfState;
};
class StellarModelTestSurface final : public mean_field::surface::SurfaceBase {
class StructureWithoutSeed final {
public:
explicit StellarModelTestSurface(std::shared_ptr<StellarModelExtensionTracker> tracker)
: m_tracker(std::move(tracker)) {
}
[[nodiscard]] const mean_field::eos::Polytrope &equationOfState() const noexcept;
[[nodiscard]]
mean_field::surface::ResolvedSurfaceCondition
resolve(const mean_field::eos::EquationOfState &equationOfState) const override {
++m_tracker->surfaceResolutionCount;
[[nodiscard]] double targetMass() const noexcept;
m_tracker->surfaceResolutionEquationOfState = &equationOfState;
return mean_field::surface::ResolvedSurfaceCondition{0.375};
}
void validate(const mean_field::eos::EquationOfState &equationOfState) const override {
++m_tracker->surfaceValidationCount;
m_tracker->surfaceValidationEquationOfState = &equationOfState;
}
private:
std::shared_ptr<StellarModelExtensionTracker> m_tracker;
void validate() const;
};
class SurfaceWithoutPhysicalQuantity final { };
struct ModelSurfaceState final {
double specificEnthalpy;
[[nodiscard]] mean_field::eos::SpecificEnthalpyValue
value(mean_field::eos::quantity::SpecificEnthalpy) const noexcept {
return mean_field::eos::SpecificEnthalpyValue{specificEnthalpy};
}
};
using PolytropicStellarModel = mean_field::models::StellarModel<mean_field::models::structure::PolytropicStructure>;
using ExtensionStellarModel = mean_field::models::StellarModel<StellarModelTestStructure>;
} // namespace
TEST_CASE(
"Stellar Model Owns Structure And Surface Prescriptions",
tags::barotrope &tags::unit &tags::model
tags::stellar_model_type_contract
) {
STATIC_REQUIRE_FALSE(std::is_copy_constructible_v<mean_field::models::StellarModel>);
STATIC_CHECK(mean_field::models::StructurePrescription<mean_field::models::structure::PolytropicStructure>);
STATIC_CHECK(mean_field::models::StructurePrescription<StellarModelTestStructure>);
STATIC_CHECK_FALSE(mean_field::models::StructurePrescription<StructureWithoutSeed>);
STATIC_REQUIRE_FALSE(std::is_copy_assignable_v<mean_field::models::StellarModel>);
STATIC_CHECK(
mean_field::models::SurfacePrescription<
mean_field::surface::ConstantPressureSurface, mean_field::eos::Polytrope>
);
STATIC_CHECK_FALSE(
mean_field::models::SurfacePrescription<SurfaceWithoutPhysicalQuantity, mean_field::eos::Polytrope>
);
STATIC_REQUIRE(std::is_nothrow_move_constructible_v<mean_field::models::StellarModel>);
STATIC_CHECK_FALSE(std::derived_from<StellarModelTestStructure, mean_field::models::structure::StructureBase>);
STATIC_CHECK_FALSE(
std::derived_from<
mean_field::models::structure::PolytropicStructure, mean_field::models::structure::StructureBase>
);
STATIC_CHECK(
std::same_as<
decltype(std::declval<const mean_field::models::structure::StructureBase &>().equationOfState()),
mean_field::eos::EquationOfStateView>
);
STATIC_REQUIRE(std::is_nothrow_move_assignable_v<mean_field::models::StellarModel>);
STATIC_REQUIRE_FALSE(std::is_copy_constructible_v<PolytropicStellarModel>);
STATIC_REQUIRE_FALSE(std::is_copy_assignable_v<PolytropicStellarModel>);
STATIC_REQUIRE(std::is_nothrow_move_constructible_v<PolytropicStellarModel>);
STATIC_REQUIRE(std::is_nothrow_move_assignable_v<PolytropicStellarModel>);
mean_field::models::StellarModel model{
mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{3.0, 0.25}, 1.0},
mean_field::surface::Isobaric{0.0}
mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.0}}
};
CHECK(model.targetMass() == 1.0);
CHECK(model.resolvedSurfaceCondition().targetEnthalpy == 0.0);
CHECK(
dynamic_cast<const mean_field::models::structure::PolytropicStructure *>(&model.structurePrescription()) !=
nullptr
STATIC_CHECK(std::same_as<decltype(model), PolytropicStellarModel>);
STATIC_CHECK(
std::same_as<
decltype(model.structurePrescription()), const mean_field::models::structure::PolytropicStructure &>
);
STATIC_CHECK(
std::same_as<decltype(model.surfacePrescription()), const mean_field::surface::ConstantPressureSurface &>
);
STATIC_CHECK(std::same_as<decltype(model.equationOfState()), const mean_field::eos::Polytrope &>);
CHECK(dynamic_cast<const mean_field::surface::Isobaric *>(&model.surfacePrescription()) != nullptr);
CHECK(model.targetMass() == 1.0);
CHECK(model.compiledSurfaceConstraint().targetPressure() == mean_field::eos::PressureValue{0.0});
CHECK(&model.equationOfState() == &model.structurePrescription().equationOfState());
CHECK(model.surfacePrescription().targetPressure() == mean_field::eos::PressureValue{0.0});
}
TEST_CASE(
@@ -135,7 +157,7 @@ TEST_CASE(
) {
mean_field::models::StellarModel model{
mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{3.0, 0.25}, 1.0},
mean_field::surface::Isobaric{}
mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.0}}
};
const mean_field::models::structure::StructureSeed seed =
@@ -153,45 +175,49 @@ TEST_CASE(
TEST_CASE(
"Moving A Stellar Model Preserves Stable Prescription Addresses",
tags::barotrope &tags::unit &tags::model
tags::barotrope &tags::unit &tags::model &tags::surface_constraint_lifetime
) {
mean_field::models::StellarModel originalModel{
mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{3.0, 0.25}, 1.0},
mean_field::surface::Isobaric{}
mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.0}}
};
const mean_field::models::structure::StructureBase *structureAddress = &originalModel.structurePrescription();
const mean_field::models::structure::PolytropicStructure *structureAddress = &originalModel.structurePrescription();
const mean_field::surface::SurfaceBase *surfaceAddress = &originalModel.surfacePrescription();
const mean_field::surface::ConstantPressureSurface *surfaceAddress = &originalModel.surfacePrescription();
const mean_field::eos::EquationOfState *equationOfStateAddress = &originalModel.equationOfState();
const mean_field::eos::Polytrope *equationOfStateAddress = &originalModel.equationOfState();
const auto *compiledSurfaceConstraintAddress = &originalModel.compiledSurfaceConstraint();
mean_field::models::StellarModel movedModel{std::move(originalModel)};
CHECK(&movedModel.structurePrescription() == structureAddress);
CHECK(&movedModel.surfacePrescription() == surfaceAddress);
CHECK(&movedModel.equationOfState() == equationOfStateAddress);
CHECK(&movedModel.compiledSurfaceConstraint() == compiledSurfaceConstraintAddress);
CHECK(movedModel.targetMass() == 1.0);
}
TEST_CASE(
"Stellar Model Resolves A Positive Isobaric Surface",
"Stellar Model Compiles A Positive Constant Pressure Surface",
tags::barotrope &tags::unit &tags::model
) {
constexpr double targetPressure = 0.03125;
mean_field::models::StellarModel model{
mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{3.0, 0.25}, 1.0},
mean_field::surface::Isobaric{targetPressure}
mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{targetPressure}}
};
const double targetEnthalpy = model.resolvedSurfaceCondition().targetEnthalpy;
const double requiredSpecificEnthalpy = mean_field::eos::evaluate<mean_field::eos::quantity::SpecificEnthalpy>(
model.equationOfState(), mean_field::eos::PressureValue{targetPressure}
)
.value();
CHECK(targetEnthalpy > 0.0);
CHECK(
std::abs(model.equationOfState().pressure_from_enthalpy(targetEnthalpy) - targetPressure) <
64.0 * std::numeric_limits<double>::epsilon()
);
CHECK(requiredSpecificEnthalpy > 0.0);
CHECK(model.compiledSurfaceConstraint().targetPressure() == mean_field::eos::PressureValue{targetPressure});
CHECK(model.compiledSurfaceConstraint().residual(ModelSurfaceState{requiredSpecificEnthalpy}) == 0.0);
}
TEST_CASE(
@@ -200,27 +226,22 @@ TEST_CASE(
) {
const auto tracker = std::make_shared<StellarModelExtensionTracker>();
mean_field::models::StellarModel model{StellarModelTestStructure{tracker}, StellarModelTestSurface{tracker}};
mean_field::models::StellarModel model{
StellarModelTestStructure{tracker},
mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.375}}
};
STATIC_CHECK(std::same_as<decltype(model), ExtensionStellarModel>);
REQUIRE(tracker->structureValidationCount == 1);
REQUIRE(tracker->surfaceValidationCount == 1);
REQUIRE(tracker->surfaceResolutionCount == 1);
CHECK(dynamic_cast<const StellarModelTestStructure *>(&model.structurePrescription()) != nullptr);
CHECK(dynamic_cast<const StellarModelTestSurface *>(&model.surfacePrescription()) != nullptr);
const mean_field::eos::EquationOfState *ownedEquationOfState = &model.equationOfState();
const mean_field::eos::Polytrope *ownedEquationOfState = &model.equationOfState();
CHECK(tracker->structureEquationOfState == ownedEquationOfState);
CHECK(tracker->surfaceValidationEquationOfState == ownedEquationOfState);
CHECK(tracker->surfaceResolutionEquationOfState == ownedEquationOfState);
CHECK(model.targetMass() == 2.5);
CHECK(model.resolvedSurfaceCondition().targetEnthalpy == 0.375);
CHECK(model.compiledSurfaceConstraint().targetPressure() == mean_field::eos::PressureValue{0.375});
}
TEST_CASE(
@@ -229,23 +250,25 @@ TEST_CASE(
) {
mean_field::models::StellarModel sourceModel{
mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{3.0, 0.25}, 1.25},
mean_field::surface::Isobaric{0.0}
mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.0}}
};
mean_field::models::StellarModel destinationModel{
mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{2.0, 0.5}, 4.0},
mean_field::surface::Isobaric{0.02}
mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.02}}
};
const mean_field::models::structure::StructureBase *sourceStructureAddress = &sourceModel.structurePrescription();
const mean_field::models::structure::PolytropicStructure *sourceStructureAddress =
&sourceModel.structurePrescription();
const mean_field::surface::SurfaceBase *sourceSurfaceAddress = &sourceModel.surfacePrescription();
const mean_field::surface::ConstantPressureSurface *sourceSurfaceAddress = &sourceModel.surfacePrescription();
const mean_field::eos::EquationOfState *sourceEquationOfStateAddress = &sourceModel.equationOfState();
const mean_field::eos::Polytrope *sourceEquationOfStateAddress = &sourceModel.equationOfState();
const double sourceTargetEnthalpy = sourceModel.resolvedSurfaceCondition().targetEnthalpy;
const mean_field::eos::PressureValue sourceTargetPressure =
sourceModel.compiledSurfaceConstraint().targetPressure();
destinationModel = std::move(sourceModel);
destinationModel = std::move(sourceModel);
CHECK(&destinationModel.structurePrescription() == sourceStructureAddress);
@@ -255,5 +278,83 @@ TEST_CASE(
CHECK(destinationModel.targetMass() == 1.25);
CHECK(destinationModel.resolvedSurfaceCondition().targetEnthalpy == sourceTargetEnthalpy);
}
CHECK(destinationModel.compiledSurfaceConstraint().targetPressure() == sourceTargetPressure);
}
TEST_CASE(
"Stellar Model View Supports Heterogeneous Typed Models",
tags::stellar_model_runtime_view
) {
const auto tracker = std::make_shared<StellarModelExtensionTracker>();
const mean_field::models::StellarModel polytropicModel{
mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{3.0, 0.25}, 1.0},
mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.0}}
};
const mean_field::models::StellarModel extensionModel{
StellarModelTestStructure{tracker},
mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.375}}
};
STATIC_CHECK(std::is_trivially_copyable_v<mean_field::models::StellarModelView>);
STATIC_CHECK_FALSE(std::constructible_from<mean_field::models::StellarModelView, PolytropicStellarModel &&>);
const std::array views{
mean_field::models::StellarModelView{polytropicModel}, mean_field::models::StellarModelView{extensionModel}
};
CHECK(views[0].targetMass() == 1.0);
CHECK(views[1].targetMass() == 2.5);
CHECK(views[1].surfaceCondition().targetPressure == 0.375);
REQUIRE(views[1].surfaceDependencies().stateFields.size() == 1);
CHECK(
views[1].surfaceDependencies().residualRowField ==
mean_field::surface::surfaceFieldId<mean_field::field::Enthalpy>
);
const auto pressure =
views[0].equationOfState().tryEvaluate<mean_field::eos::quantity::Pressure>(mean_field::eos::DensityValue{0.7});
REQUIRE(pressure.has_value());
CHECK(
pressure->value() == mean_field::eos::evaluate<mean_field::eos::quantity::Pressure>(
polytropicModel.equationOfState(), mean_field::eos::DensityValue{0.7}
)
.value()
);
const mean_field::models::structure::StructureSeed seed =
views[1].makeInitialSeed({.centralDensity = 1.75, .radialSampleCount = 2});
CHECK(seed.centralDensity == 1.75);
CHECK(seed.radius.Size() == 2);
}
TEST_CASE(
"Stellar Model View Retains Stable Pointees When Its Owner Moves",
tags::stellar_model_runtime_view
) {
mean_field::models::StellarModel originalModel{
mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{3.0, 0.25}, 1.0},
mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.0}}
};
const mean_field::models::StellarModelView view{originalModel};
PolytropicStellarModel movedModel{std::move(originalModel)};
const auto pressure =
view.equationOfState().tryEvaluate<mean_field::eos::quantity::Pressure>(mean_field::eos::DensityValue{0.7});
const mean_field::models::structure::StructureSeed seed =
view.makeInitialSeed({.centralDensity = 1.0, .radialSampleCount = 8});
REQUIRE(pressure.has_value());
CHECK(view.targetMass() == movedModel.targetMass());
CHECK(
pressure->value() == mean_field::eos::evaluate<mean_field::eos::quantity::Pressure>(
movedModel.equationOfState(), mean_field::eos::DensityValue{0.7}
)
.value()
);
CHECK(seed.radius.Size() == 8);
}

View File

@@ -164,9 +164,7 @@ TEST_CASE(
CHECK_FALSE(gravityPotentialReport.updatedDisplacement);
CHECK_FALSE(gravityPotentialReport.updatedBernoulliConstant);
CHECK(
context.GetBaseGravityPotentialTrue()(context.GetGravityPotentialMap().true_dof(0)) == gravityPotential(0)
);
CHECK(context.GetBaseGravityPotentialTrue()(context.GetGravityPotentialMap().true_dof(0)) == gravityPotential(0));
++dependencies.bernoulliConstant.revision;

File diff suppressed because it is too large Load Diff

View File

@@ -11,386 +11,370 @@ import mean_field;
import test_helpers;
namespace gravity_displacement_force_analytic_test_utils {
struct AffineCase {
const char *name;
std::array<double, 3> scales;
};
struct AffineCase {
const char *name;
std::array<double, 3> scales;
};
[[nodiscard]] double analytic_sphere_volume(const double radius) {
return (4.0 / 3.0) * std::numbers::pi * radius * radius * radius;
}
[[nodiscard]] double analytic_sphere_volume(const double radius) {
return (4.0 / 3.0) * std::numbers::pi * radius * radius * radius;
}
[[nodiscard]] double determinant(const std::array<double, 3> &scales) {
return scales[0] * scales[1] * scales[2];
}
[[nodiscard]] double determinant(
const std::array<
double,
3> &scales
) {
return scales[0] * scales[1] * scales[2];
}
[[nodiscard]] double relative_scalar_error(const double computed,
const double expected) {
return std::abs(computed - expected) / std::max(std::abs(expected), 1.0e-30);
}
[[nodiscard]] double relative_scalar_error(
const double computed,
const double expected
) {
return std::abs(computed - expected) / std::max(std::abs(expected), 1.0e-30);
}
[[nodiscard]] mfem::Vector make_constant_density(const mean_field::fem::FEM &f,
const double densityValue) {
mfem::ParGridFunction densityField(f.densityFes.get());
mfem::ConstantCoefficient densityCoefficient(densityValue);
densityField.ProjectCoefficient(densityCoefficient);
[[nodiscard]] mfem::Vector make_constant_density(
const mean_field::fem::FEM &f,
const double densityValue
) {
mfem::ParGridFunction densityField(f.densityFes.get());
mfem::ConstantCoefficient densityCoefficient(densityValue);
densityField.ProjectCoefficient(densityCoefficient);
mfem::Vector densityTrue;
densityField.GetTrueDofs(densityTrue);
return densityTrue;
}
mfem::Vector densityTrue;
densityField.GetTrueDofs(densityTrue);
return densityTrue;
}
[[nodiscard]] mfem::Vector
make_reference_gravity(const mean_field::fem::FEM &f,
const std::array<double, 3> &referenceGravity) {
mfem::ParGridFunction gravityField(f.gravityFluxFes.get());
[[nodiscard]] mfem::Vector make_reference_gravity(
const mean_field::fem::FEM &f,
const std::array<
double,
3> &referenceGravity
) {
mfem::ParGridFunction gravityField(f.gravityFluxFes.get());
mfem::VectorFunctionCoefficient gravityCoefficient(
f.mesh->Dimension(),
[referenceGravity](const mfem::Vector &, mfem::Vector &value) {
value.SetSize(3);
mfem::VectorFunctionCoefficient gravityCoefficient(
f.mesh->Dimension(), [referenceGravity](const mfem::Vector &, mfem::Vector &value) {
value.SetSize(3);
for (int component = 0; component < 3; ++component) {
value(component) =
referenceGravity[static_cast<std::size_t>(component)];
}
});
for (int component = 0; component < 3; ++component) {
value(component) = referenceGravity[static_cast<std::size_t>(component)];
}
}
);
gravityField.ProjectCoefficient(gravityCoefficient);
gravityField.ProjectCoefficient(gravityCoefficient);
mfem::Vector gravityTrue;
gravityField.GetTrueDofs(gravityTrue);
return gravityTrue;
}
mfem::Vector gravityTrue;
gravityField.GetTrueDofs(gravityTrue);
return gravityTrue;
}
[[nodiscard]] mfem::Vector make_radial_gravity(const mean_field::fem::FEM &f,
const double radialCoefficient) {
mfem::ParGridFunction gravityField(f.gravityFluxFes.get());
[[nodiscard]] mfem::Vector make_radial_gravity(
const mean_field::fem::FEM &f,
const double radialCoefficient
) {
mfem::ParGridFunction gravityField(f.gravityFluxFes.get());
mfem::VectorFunctionCoefficient gravityCoefficient(
f.mesh->Dimension(),
[radialCoefficient](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(position.Size());
mfem::VectorFunctionCoefficient gravityCoefficient(
f.mesh->Dimension(), [radialCoefficient](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(position.Size());
for (int component = 0; component < position.Size(); ++component) {
value(component) = radialCoefficient * position(component);
}
});
for (int component = 0; component < position.Size(); ++component) {
value(component) = radialCoefficient * position(component);
}
}
);
gravityField.ProjectCoefficient(gravityCoefficient);
gravityField.ProjectCoefficient(gravityCoefficient);
mfem::Vector gravityTrue;
gravityField.GetTrueDofs(gravityTrue);
return gravityTrue;
}
mfem::Vector gravityTrue;
gravityField.GetTrueDofs(gravityTrue);
return gravityTrue;
}
[[nodiscard]] mfem::Vector
make_affine_displacement(const mean_field::fem::FEM &f,
const std::array<double, 3> &scales) {
mfem::ParGridFunction displacementField(f.displacementFes.get());
[[nodiscard]] mfem::Vector make_affine_displacement(
const mean_field::fem::FEM &f,
const std::array<
double,
3> &scales
) {
mfem::ParGridFunction displacementField(f.displacementFes.get());
mfem::VectorFunctionCoefficient displacementCoefficient(
f.mesh->Dimension(),
[scales](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(position.Size());
mfem::VectorFunctionCoefficient displacementCoefficient(
f.mesh->Dimension(), [scales](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(position.Size());
for (int component = 0; component < position.Size(); ++component) {
value(component) =
(scales[static_cast<std::size_t>(component)] - 1.0) *
position(component);
}
});
for (int component = 0; component < position.Size(); ++component) {
value(component) = (scales[static_cast<std::size_t>(component)] - 1.0) * position(component);
}
}
);
displacementField.ProjectCoefficient(displacementCoefficient);
displacementField.ProjectCoefficient(displacementCoefficient);
mfem::Vector displacementTrue;
displacementField.GetTrueDofs(displacementTrue);
return displacementTrue;
}
mfem::Vector displacementTrue;
displacementField.GetTrueDofs(displacementTrue);
return displacementTrue;
}
[[nodiscard]] mfem::Vector
make_constant_test_direction(const mean_field::fem::FEM &f,
const int selectedComponent) {
mfem::ParGridFunction testField(f.displacementFes.get());
[[nodiscard]] mfem::Vector make_constant_test_direction(
const mean_field::fem::FEM &f,
const int selectedComponent
) {
mfem::ParGridFunction testField(f.displacementFes.get());
mfem::VectorFunctionCoefficient testCoefficient(
f.mesh->Dimension(),
[selectedComponent](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(position.Size());
value = 0.0;
value(selectedComponent) = 1.0;
});
mfem::VectorFunctionCoefficient testCoefficient(
f.mesh->Dimension(), [selectedComponent](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(position.Size());
value = 0.0;
value(selectedComponent) = 1.0;
}
);
testField.ProjectCoefficient(testCoefficient);
testField.ProjectCoefficient(testCoefficient);
mfem::Vector testTrue;
testField.GetTrueDofs(testTrue);
return testTrue;
}
mfem::Vector testTrue;
testField.GetTrueDofs(testTrue);
return testTrue;
}
[[nodiscard]] mfem::Vector
make_dilation_test_direction(const mean_field::fem::FEM &f) {
mfem::ParGridFunction testField(f.displacementFes.get());
[[nodiscard]] mfem::Vector make_dilation_test_direction(const mean_field::fem::FEM &f) {
mfem::ParGridFunction testField(f.displacementFes.get());
mfem::VectorFunctionCoefficient testCoefficient(
f.mesh->Dimension(), [](const mfem::Vector &position,
mfem::Vector &value) { value = position; });
mfem::VectorFunctionCoefficient testCoefficient(
f.mesh->Dimension(), [](const mfem::Vector &position, mfem::Vector &value) { value = position; }
);
testField.ProjectCoefficient(testCoefficient);
testField.ProjectCoefficient(testCoefficient);
mfem::Vector testTrue;
testField.GetTrueDofs(testTrue);
return testTrue;
}
mfem::Vector testTrue;
testField.GetTrueDofs(testTrue);
return testTrue;
}
void set_mass_normalized_density(mean_field::fem::FEM &f,
const double targetMass,
mfem::ParGridFunction &densityField) {
const mfem::Vector stellarDensityTrue =
gravity_prepared_test_utils::make_domain_supported_density(f, true);
void set_mass_normalized_density(
mean_field::fem::FEM &f,
const double targetMass,
mfem::ParGridFunction &densityField
) {
const mfem::Vector stellarDensityTrue = gravity_prepared_test_utils::make_domain_supported_density(f, true);
densityField.SetFromTrueDofs(stellarDensityTrue);
densityField.SetFromTrueDofs(stellarDensityTrue);
const double unnormalizedMass =
mean_field::analysis::domain_integrate_grid_function(
f, densityField, mean_field::utils::DOMAINS::STELLAR);
const double unnormalizedMass =
mean_field::analysis::domain_integrate_grid_function(f, densityField, mean_field::utils::DOMAINS::STELLAR);
MFEM_VERIFY(unnormalizedMass > 0.0,
"The analytic gravity-force test obtained non-positive mass.");
MFEM_VERIFY(unnormalizedMass > 0.0, "The analytic gravity-force test obtained non-positive mass.");
densityField *= targetMass / unnormalizedMass;
}
densityField *= targetMass / unnormalizedMass;
}
} // namespace gravity_displacement_force_analytic_test_utils
TEST_CASE("Gravity Displacement Force Matches Analytic Affine Resultants",
tags::gravity &tags::accuracy &tags::analytic_comparison
&tags::integration) {
mean_field::utils::Args args = test_utils::setup_args();
TEST_CASE(
"Gravity Displacement Force Matches Analytic Affine Resultants",
tags::gravity &tags::accuracy &tags::analytic_comparison &tags::integration
) {
mean_field::utils::Args args = test_utils::setup_args();
mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0);
mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.okay());
REQUIRE(f.domainMapperStateless != nullptr);
REQUIRE(f.domainMapperStateless != nullptr);
REQUIRE(f.okay());
REQUIRE(f.domainMapperStateless != nullptr);
REQUIRE(f.domainMapperStateless != nullptr);
constexpr double densityValue = 1.37;
constexpr double densityValue = 1.37;
constexpr std::array<double, 3> physicalGravity{0.31, -0.47, 0.22};
constexpr std::array<double, 3> physicalGravity{0.31, -0.47, 0.22};
constexpr std::array<
gravity_displacement_force_analytic_test_utils::AffineCase, 3>
affineCases{{{.name = "identity geometry", .scales = {1.0, 1.0, 1.0}},
{.name = "volume-preserving affine geometry",
.scales = {1.14, 0.93, 1.0 / (1.14 * 0.93)}},
{.name = "volume-changing affine geometry",
.scales = {1.11, 0.96, 1.07}}}};
constexpr std::array<gravity_displacement_force_analytic_test_utils::AffineCase, 3> affineCases{
{{.name = "identity geometry", .scales = {1.0, 1.0, 1.0}},
{.name = "volume-preserving affine geometry", .scales = {1.14, 0.93, 1.0 / (1.14 * 0.93)}},
{.name = "volume-changing affine geometry", .scales = {1.11, 0.96, 1.07}}}
};
const mfem::Vector density =
gravity_displacement_force_analytic_test_utils::make_constant_density(
f, densityValue);
const mfem::Vector density = gravity_displacement_force_analytic_test_utils::make_constant_density(f, densityValue);
const double referenceVolume =
gravity_displacement_force_analytic_test_utils::analytic_sphere_volume(
mean_field::utils::RADIUS);
const double referenceVolume =
gravity_displacement_force_analytic_test_utils::analytic_sphere_volume(mean_field::utils::RADIUS);
constexpr double relativeTolerance = 5.0e-6;
constexpr double relativeTolerance = 5.0e-6;
for (const gravity_displacement_force_analytic_test_utils::AffineCase
&affineCase : affineCases) {
DYNAMIC_SECTION(affineCase.name) {
const double mapDeterminant =
gravity_displacement_force_analytic_test_utils::determinant(
affineCase.scales);
for (const gravity_displacement_force_analytic_test_utils::AffineCase &affineCase : affineCases) {
DYNAMIC_SECTION(affineCase.name) {
const double mapDeterminant =
gravity_displacement_force_analytic_test_utils::determinant(affineCase.scales);
REQUIRE(mapDeterminant > 0.0);
REQUIRE(mapDeterminant > 0.0);
std::array<double, 3> referenceGravity{};
std::array<double, 3> referenceGravity{};
/*
* For x = A X, the H(div) Piola relation is
*
* g_phys = A g_ref / det(A).
*
* Prescribe the RT pullback that represents the requested
* constant physical gravity field exactly.
*/
for (int component = 0; component < 3; ++component) {
referenceGravity[static_cast<std::size_t>(component)] =
mapDeterminant *
physicalGravity[static_cast<std::size_t>(component)] /
affineCase.scales[static_cast<std::size_t>(component)];
}
/*
* For x = A X, the H(div) Piola relation is
*
* g_phys = A g_ref / det(A).
*
* Prescribe the RT pullback that represents the requested
* constant physical gravity field exactly.
*/
for (int component = 0; component < 3; ++component) {
referenceGravity[static_cast<std::size_t>(component)] =
mapDeterminant * physicalGravity[static_cast<std::size_t>(component)] /
affineCase.scales[static_cast<std::size_t>(component)];
}
const mfem::Vector gravityGradient =
gravity_displacement_force_analytic_test_utils::
make_reference_gravity(f, referenceGravity);
const mfem::Vector gravityGradient =
gravity_displacement_force_analytic_test_utils::make_reference_gravity(f, referenceGravity);
const mfem::Vector displacement =
gravity_displacement_force_analytic_test_utils::
make_affine_displacement(f, affineCase.scales);
const mfem::Vector displacement =
gravity_displacement_force_analytic_test_utils::make_affine_displacement(f, affineCase.scales);
mfem::Vector residual;
mfem::Vector residual;
mean_field::operators::kernels::apply_gravity_displacement_force_residual(
f, *f.domainMapperStateless, density, gravityGradient, displacement,
residual);
mean_field::operators::kernels::apply_gravity_displacement_force_residual(
f, *f.domainMapperStateless, density, gravityGradient, displacement, residual
);
for (int component = 0; component < 3; ++component) {
const mfem::Vector testDirection =
gravity_displacement_force_analytic_test_utils::
make_constant_test_direction(f, component);
for (int component = 0; component < 3; ++component) {
const mfem::Vector testDirection =
gravity_displacement_force_analytic_test_utils::make_constant_test_direction(f, component);
const double computedResultant =
gravity_prepared_test_utils::global_dot(residual, testDirection,
f.mesh->GetComm());
const double computedResultant =
gravity_prepared_test_utils::global_dot(residual, testDirection, f.mesh->GetComm());
const double expectedResultant =
densityValue *
physicalGravity[static_cast<std::size_t>(component)] *
mapDeterminant * referenceVolume;
const double expectedResultant = densityValue * physicalGravity[static_cast<std::size_t>(component)] *
mapDeterminant * referenceVolume;
const double relativeError =
gravity_displacement_force_analytic_test_utils::
relative_scalar_error(computedResultant, expectedResultant);
const double relativeError = gravity_displacement_force_analytic_test_utils::relative_scalar_error(
computedResultant, expectedResultant
);
CAPTURE(component);
INFO("Map determinant = " << mapDeterminant);
INFO("Computed resultant = " << computedResultant);
INFO("Analytic resultant = " << expectedResultant);
INFO("Relative resultant error = " << relativeError);
CAPTURE(component);
INFO("Map determinant = " << mapDeterminant);
INFO("Computed resultant = " << computedResultant);
INFO("Analytic resultant = " << expectedResultant);
INFO("Relative resultant error = " << relativeError);
CHECK(relativeError < relativeTolerance);
}
CHECK(relativeError < relativeTolerance);
}
}
}
}
}
TEST_CASE(
"Gravity Displacement Force Reproduces Analytic Homogeneous Sphere Work",
tags::gravity &tags::accuracy &tags::analytic_comparison
&tags::integration) {
mean_field::utils::Args args = test_utils::setup_args();
tags::gravity &tags::accuracy &tags::analytic_comparison &tags::integration
) {
mean_field::utils::Args args = test_utils::setup_args();
mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0);
mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.okay());
REQUIRE(f.domainMapperStateless != nullptr);
REQUIRE(f.okay());
REQUIRE(f.domainMapperStateless != nullptr);
const double radius = mean_field::utils::RADIUS;
const double mass = mean_field::utils::MASS;
const double volume =
gravity_displacement_force_analytic_test_utils::analytic_sphere_volume(
radius);
const double radius = mean_field::utils::RADIUS;
const double mass = mean_field::utils::MASS;
const double volume = gravity_displacement_force_analytic_test_utils::analytic_sphere_volume(radius);
const double densityValue = mass / volume;
const double radialGravityCoefficient =
mean_field::utils::G * mass / (radius * radius * radius);
const double densityValue = mass / volume;
const double radialGravityCoefficient = mean_field::utils::G * mass / (radius * radius * radius);
const mfem::Vector density =
gravity_displacement_force_analytic_test_utils::make_constant_density(
f, densityValue);
const mfem::Vector density = gravity_displacement_force_analytic_test_utils::make_constant_density(f, densityValue);
const mfem::Vector gravityGradient =
gravity_displacement_force_analytic_test_utils::make_radial_gravity(
f, radialGravityCoefficient);
const mfem::Vector gravityGradient =
gravity_displacement_force_analytic_test_utils::make_radial_gravity(f, radialGravityCoefficient);
mfem::Vector displacement(f.displacementFes->GetTrueVSize());
displacement = 0.0;
mfem::Vector displacement(f.displacementFes->GetTrueVSize());
displacement = 0.0;
mfem::Vector residual;
mfem::Vector residual;
mean_field::operators::kernels::apply_gravity_displacement_force_residual(
f, *f.domainMapperStateless, density, gravityGradient, displacement,
residual);
mean_field::operators::kernels::apply_gravity_displacement_force_residual(
f, *f.domainMapperStateless, density, gravityGradient, displacement, residual
);
const mfem::Vector dilationDirection =
gravity_displacement_force_analytic_test_utils::
make_dilation_test_direction(f);
const mfem::Vector dilationDirection =
gravity_displacement_force_analytic_test_utils::make_dilation_test_direction(f);
const double computedWork = gravity_prepared_test_utils::global_dot(
residual, dilationDirection, f.mesh->GetComm());
const double computedWork = gravity_prepared_test_utils::global_dot(residual, dilationDirection, f.mesh->GetComm());
const double analyticWork =
(3.0 / 5.0) * mean_field::utils::G * mass * mass / radius;
const double analyticWork = (3.0 / 5.0) * mean_field::utils::G * mass * mass / radius;
const double relativeError =
gravity_displacement_force_analytic_test_utils::relative_scalar_error(
computedWork, analyticWork);
const double relativeError =
gravity_displacement_force_analytic_test_utils::relative_scalar_error(computedWork, analyticWork);
INFO("Computed positive gravity work = " << computedWork);
INFO("Analytic positive gravity work = " << analyticWork);
INFO("Computed gravitational virial = " << -computedWork);
INFO("Analytic binding energy = " << -analyticWork);
INFO("Relative analytic work error = " << relativeError);
INFO("Computed positive gravity work = " << computedWork);
INFO("Analytic positive gravity work = " << analyticWork);
INFO("Computed gravitational virial = " << -computedWork);
INFO("Analytic binding energy = " << -analyticWork);
INFO("Relative analytic work error = " << relativeError);
REQUIRE(computedWork > 0.0);
CHECK(relativeError < 1.0e-5);
REQUIRE(computedWork > 0.0);
CHECK(relativeError < 1.0e-5);
}
TEST_CASE("Solved Homogeneous Sphere Gravity Force Matches Analytic Virial",
tags::gravity &tags::accuracy &tags::analytic_comparison
&tags::integration &tags::initialization) {
mean_field::utils::Args args = test_utils::setup_args();
args.p.rtol = 1.0e-13;
args.p.max_iters = std::max(args.p.max_iters, 1000);
TEST_CASE(
"Solved Homogeneous Sphere Gravity Force Matches Analytic Virial",
tags::gravity &tags::accuracy &tags::analytic_comparison &tags::integration &tags::initialization
) {
mean_field::utils::Args args = test_utils::setup_args();
args.p.rtol = 1.0e-13;
args.p.max_iters = std::max(args.p.max_iters, 1000);
mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0);
mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.okay());
REQUIRE(f.domainMapperStateless != nullptr);
REQUIRE(f.okay());
REQUIRE(f.domainMapperStateless != nullptr);
mfem::ParGridFunction displacementField(f.displacementFes.get());
displacementField = 0.0;
mfem::ParGridFunction displacementField(f.displacementFes.get());
displacementField = 0.0;
REQUIRE(f.domainMapperStateless != nullptr);
*f.displacement = 0.0;
REQUIRE(f.domainMapperStateless != nullptr);
*f.displacement = 0.0;
const double radius = mean_field::utils::RADIUS;
const double mass = mean_field::utils::MASS;
const double radius = mean_field::utils::RADIUS;
const double mass = mean_field::utils::MASS;
mfem::ParGridFunction densityField(f.densityFes.get());
mfem::ParGridFunction densityField(f.densityFes.get());
gravity_displacement_force_analytic_test_utils::set_mass_normalized_density(
f, mass, densityField);
gravity_displacement_force_analytic_test_utils::set_mass_normalized_density(f, mass, densityField);
const mean_field::physics::GravitySolution gravitySolution =
mean_field::physics::solve_gravity_field(f, args, densityField,
displacementField);
const mean_field::physics::GravitySolution gravitySolution =
mean_field::physics::solve_gravity_field(f, args, densityField, displacementField);
mfem::Vector densityTrue;
mfem::Vector gravityGradientTrue;
mfem::Vector displacementTrue;
mfem::Vector densityTrue;
mfem::Vector gravityGradientTrue;
mfem::Vector displacementTrue;
densityField.GetTrueDofs(densityTrue);
gravitySolution.gradPhi.GetTrueDofs(gravityGradientTrue);
displacementField.GetTrueDofs(displacementTrue);
densityField.GetTrueDofs(densityTrue);
gravitySolution.gradPhi.GetTrueDofs(gravityGradientTrue);
displacementField.GetTrueDofs(displacementTrue);
mfem::Vector residual;
mfem::Vector residual;
mean_field::operators::kernels::apply_gravity_displacement_force_residual(
f, *f.domainMapperStateless, densityTrue, gravityGradientTrue,
displacementTrue, residual);
mean_field::operators::kernels::apply_gravity_displacement_force_residual(
f, *f.domainMapperStateless, densityTrue, gravityGradientTrue, displacementTrue, residual
);
const mfem::Vector dilationDirection =
gravity_displacement_force_analytic_test_utils::
make_dilation_test_direction(f);
const mfem::Vector dilationDirection =
gravity_displacement_force_analytic_test_utils::make_dilation_test_direction(f);
const double computedWork = gravity_prepared_test_utils::global_dot(
residual, dilationDirection, f.mesh->GetComm());
const double computedWork = gravity_prepared_test_utils::global_dot(residual, dilationDirection, f.mesh->GetComm());
const double analyticWork =
(3.0 / 5.0) * mean_field::utils::G * mass * mass / radius;
const double analyticWork = (3.0 / 5.0) * mean_field::utils::G * mass * mass / radius;
const double relativeError =
gravity_displacement_force_analytic_test_utils::relative_scalar_error(
computedWork, analyticWork);
const double relativeError =
gravity_displacement_force_analytic_test_utils::relative_scalar_error(computedWork, analyticWork);
INFO("Solved-field positive gravity work = " << computedWork);
INFO("Analytic positive gravity work = " << analyticWork);
INFO("Solved-field gravitational virial = " << -computedWork);
INFO("Analytic homogeneous-sphere binding energy = " << -analyticWork);
INFO("Relative solved-field virial error = " << relativeError);
INFO("Solved-field positive gravity work = " << computedWork);
INFO("Analytic positive gravity work = " << analyticWork);
INFO("Solved-field gravitational virial = " << -computedWork);
INFO("Analytic homogeneous-sphere binding energy = " << -analyticWork);
INFO("Relative solved-field virial error = " << relativeError);
REQUIRE(computedWork > 0.0);
CHECK(relativeError < 1.0e-5);
REQUIRE(computedWork > 0.0);
CHECK(relativeError < 1.0e-5);
}

File diff suppressed because it is too large Load Diff

View File

@@ -72,7 +72,10 @@ TEST_CASE(
constexpr double enthalpyValue = 0.8;
const double densityValue = barotrope.density_from_enthalpy(enthalpyValue);
const double densityValue = mean_field::eos::evaluate<mean_field::eos::quantity::Density>(
barotrope, mean_field::eos::SpecificEnthalpyValue{enthalpyValue}
)
.value();
const mfem::Vector enthalpy = project_constant(*f.enthalpyFes, enthalpyValue);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -242,8 +242,12 @@ namespace prepared_barotropic_closure_test_utils {
const ClosureCondition &condition
) {
mfem::FunctionCoefficient coefficient([&equationOfState, condition](const mfem::Vector &position) {
const double enthalpy = evaluate_enthalpy(position, condition);
return condition.densityFactor * equationOfState.density_from_enthalpy(enthalpy) + condition.densityOffset +
const double enthalpy = evaluate_enthalpy(position, condition);
const double equationOfStateDensity = mean_field::eos::evaluate<mean_field::eos::quantity::Density>(
equationOfState, mean_field::eos::SpecificEnthalpyValue{enthalpy}
)
.value();
return condition.densityFactor * equationOfStateDensity + condition.densityOffset +
condition.densityGradient * (0.40 * position(0) + 0.25 * position(1) - 0.15 * position(2));
});
return project_scalar(*f.densityFes, coefficient);
@@ -784,8 +788,12 @@ namespace prepared_barotropic_closure_test_utils {
const Maps maps(f);
const mean_field::eos::Polytrope equationOfState(3.0, 1.5);
constexpr double enthalpyValue = 1.20;
const double equilibriumDensityValue = equationOfState.density_from_enthalpy(enthalpyValue);
constexpr double enthalpyValue = 1.20;
const double equilibriumDensityValue =
mean_field::eos::evaluate<mean_field::eos::quantity::Density>(
equationOfState, mean_field::eos::SpecificEnthalpyValue{enthalpyValue}
)
.value();
const mfem::Vector enthalpy = reduce(maps.enthalpy, make_constant_field(*f.enthalpyFes, enthalpyValue));
const mfem::Vector equilibriumDensity =

View File

@@ -10,181 +10,157 @@ using namespace mean_field;
using Catch::Matchers::WithinAbs;
namespace prepared_test = gravity_prepared_test_utils;
TEST_CASE("Prepared Mapped Hdiv Mass Matches Stateless Kernel",
tags::gravity_prepared) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
TEST_CASE(
"Prepared Mapped Hdiv Mass Matches Stateless Kernel",
tags::gravity_prepared
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
operators::PreparedMappedHDivMassOperator prepared_operator(
f, *f.domainMapperStateless);
REQUIRE(prepared_operator.Width() ==
prepared_operator.GetFluxMap().reduced_size());
REQUIRE(prepared_operator.Height() ==
prepared_operator.GetFluxMap().reduced_size());
operators::PreparedMappedHDivMassOperator prepared_operator(f, *f.domainMapperStateless);
REQUIRE(prepared_operator.Width() == prepared_operator.GetFluxMap().reduced_size());
REQUIRE(prepared_operator.Height() == prepared_operator.GetFluxMap().reduced_size());
const mfem::Vector gravity_gradient_true =
prepared_test::make_deterministic_vector(f.gravityFluxFes->GetTrueVSize(),
0.21);
const mfem::Vector gravity_gradient =
prepared_operator.GetFluxMap().gather(gravity_gradient_true);
const MPI_Comm communicator = f.gravityFluxFes->GetComm();
const mfem::Vector gravity_gradient_true =
prepared_test::make_deterministic_vector(f.gravityFluxFes->GetTrueVSize(), 0.21);
const mfem::Vector gravity_gradient = prepared_operator.GetFluxMap().gather(gravity_gradient_true);
const MPI_Comm communicator = f.gravityFluxFes->GetComm();
mfem::Vector identity_action;
mfem::Vector deformed_action;
mfem::Vector identity_action;
mfem::Vector deformed_action;
for (const double deformation_scale : {0.0, 1.0}) {
const mfem::Vector displacement_true =
prepared_test::make_displacement(f, deformation_scale);
for (const double deformation_scale : {0.0, 1.0}) {
const mfem::Vector displacement_true = prepared_test::make_displacement(f, deformation_scale);
const mfem::Vector displacement = prepared_operator.GetDisplacementMap().gather(displacement_true);
prepared_operator.Prepare(displacement);
mfem::Vector prepared_action;
prepared_operator.Mult(gravity_gradient, prepared_action);
mfem::Vector reference_action_true;
operators::kernels::apply_mapped_hdiv_mass(
f, *f.domainMapperStateless, gravity_gradient_true, displacement_true, reference_action_true
);
const mfem::Vector reference_action = prepared_operator.GetFluxMap().gather(reference_action_true);
const double relative_error = prepared_test::relative_error(prepared_action, reference_action, communicator);
INFO("Deformation scale = " << deformation_scale);
INFO("Prepared action norm = " << prepared_test::global_norm(prepared_action, communicator));
INFO("Reference action norm = " << prepared_test::global_norm(reference_action, communicator));
INFO("Relative prepared-operator error = " << relative_error);
REQUIRE(prepared_operator.IsPrepared());
CHECK_THAT(relative_error, WithinAbs(0.0, 2.0e-11));
if (deformation_scale == 0.0) {
identity_action = prepared_action;
} else {
deformed_action = prepared_action;
}
}
const double geometry_change = prepared_test::relative_error(deformed_action, identity_action, communicator);
INFO("Relative action change under deformation = " << geometry_change);
CHECK(prepared_operator.GetPreparationCount() == 2);
CHECK(geometry_change > 1.0e-5);
}
TEST_CASE(
"Prepared Mapped Hdiv Mass Preserves Operator Identities",
tags::gravity_prepared
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
operators::PreparedMappedHDivMassOperator prepared_operator(f, *f.domainMapperStateless);
REQUIRE(prepared_operator.Width() == prepared_operator.GetFluxMap().reduced_size());
REQUIRE(prepared_operator.Height() == prepared_operator.GetFluxMap().reduced_size());
const mfem::Vector displacement =
prepared_operator.GetDisplacementMap().gather(displacement_true);
prepared_operator.GetDisplacementMap().gather(prepared_test::make_displacement(f, 1.0));
prepared_operator.Prepare(displacement);
mfem::Vector prepared_action;
const mfem::Vector first = prepared_operator.GetFluxMap().gather(
prepared_test::make_deterministic_vector(f.gravityFluxFes->GetTrueVSize(), 0.17)
);
const mfem::Vector second = prepared_operator.GetFluxMap().gather(
prepared_test::make_deterministic_vector(f.gravityFluxFes->GetTrueVSize(), 0.83)
);
const mfem::Vector combination = prepared_test::linear_combination(first, 1.7, second, -0.4);
prepared_operator.Mult(gravity_gradient, prepared_action);
mfem::Vector reference_action_true;
operators::kernels::apply_mapped_hdiv_mass(
f, *f.domainMapperStateless, gravity_gradient_true, displacement_true,
reference_action_true);
const mfem::Vector reference_action =
prepared_operator.GetFluxMap().gather(reference_action_true);
mfem::Vector first_action;
mfem::Vector second_action;
mfem::Vector combination_action;
mfem::Vector zero_action;
const double relative_error = prepared_test::relative_error(
prepared_action, reference_action, communicator);
prepared_operator.Mult(first, first_action);
prepared_operator.Mult(second, second_action);
prepared_operator.Mult(combination, combination_action);
INFO("Deformation scale = " << deformation_scale);
INFO("Prepared action norm = "
<< prepared_test::global_norm(prepared_action, communicator));
INFO("Reference action norm = "
<< prepared_test::global_norm(reference_action, communicator));
INFO("Relative prepared-operator error = " << relative_error);
mfem::Vector expected_combination = prepared_test::linear_combination(first_action, 1.7, second_action, -0.4);
REQUIRE(prepared_operator.IsPrepared());
CHECK_THAT(relative_error, WithinAbs(0.0, 2.0e-11));
mfem::Vector zero(first.Size());
zero = 0.0;
prepared_operator.Mult(zero, zero_action);
if (deformation_scale == 0.0) {
identity_action = prepared_action;
} else {
deformed_action = prepared_action;
const MPI_Comm communicator = f.gravityFluxFes->GetComm();
const double first_second_product = prepared_test::global_dot(first, second_action, communicator);
const double second_first_product = prepared_test::global_dot(second, first_action, communicator);
const double symmetry_error = prepared_test::relative_scalar_error(first_second_product, second_first_product);
const double linearity_error =
prepared_test::relative_error(combination_action, expected_combination, communicator);
const double first_energy = prepared_test::global_dot(first, first_action, communicator);
const double second_energy = prepared_test::global_dot(second, second_action, communicator);
const std::uint64_t preparation_count = prepared_operator.GetPreparationCount();
mfem::Vector repeated_action;
prepared_operator.Mult(first, repeated_action);
INFO("u^T M v = " << first_second_product);
INFO("v^T M u = " << second_first_product);
INFO("Relative symmetry error = " << symmetry_error);
INFO("Relative linearity error = " << linearity_error);
INFO("u^T M u = " << first_energy);
INFO("v^T M v = " << second_energy);
CHECK_THAT(symmetry_error, WithinAbs(0.0, 2.0e-12));
CHECK_THAT(linearity_error, WithinAbs(0.0, 2.0e-12));
CHECK_THAT(prepared_test::global_norm(zero_action, communicator), WithinAbs(0.0, 1.0e-14));
CHECK(first_energy > 0.0);
CHECK(second_energy > 0.0);
CHECK(prepared_test::relative_error(repeated_action, first_action, communicator) < 2.0e-14);
CHECK(prepared_operator.GetPreparationCount() == preparation_count);
}
TEST_CASE(
"Prepared Mapped Hdiv Mass Diagonal Is Positive Across Both Domains",
tags::gravity_prepared
) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
operators::PreparedMappedHDivMassOperator prepared_operator(f, *f.domainMapperStateless);
const mfem::Vector displacement =
prepared_operator.GetDisplacementMap().gather(prepared_test::make_displacement(f, 1.0));
prepared_operator.Prepare(displacement);
mfem::Vector diagonal;
mfem::Vector true_diagonal;
prepared_operator.AssembleDiagonal(diagonal);
prepared_operator.AssembleTrueDiagonal(true_diagonal);
REQUIRE(diagonal.Size() == prepared_operator.Height());
REQUIRE(true_diagonal.Size() == prepared_operator.GetFluxMap().full_size());
const mfem::Vector gathered_true_diagonal = prepared_operator.GetFluxMap().gather(true_diagonal);
for (int i = 0; i < diagonal.Size(); ++i) {
REQUIRE(std::isfinite(diagonal(i)));
CHECK(diagonal(i) > 0.0);
CHECK_THAT(diagonal(i), WithinAbs(gathered_true_diagonal(i), 1.0e-14 * std::abs(diagonal(i))));
}
}
const double geometry_change = prepared_test::relative_error(
deformed_action, identity_action, communicator);
INFO("Relative action change under deformation = " << geometry_change);
CHECK(prepared_operator.GetPreparationCount() == 2);
CHECK(geometry_change > 1.0e-5);
}
TEST_CASE("Prepared Mapped Hdiv Mass Preserves Operator Identities",
tags::gravity_prepared) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
operators::PreparedMappedHDivMassOperator prepared_operator(
f, *f.domainMapperStateless);
REQUIRE(prepared_operator.Width() ==
prepared_operator.GetFluxMap().reduced_size());
REQUIRE(prepared_operator.Height() ==
prepared_operator.GetFluxMap().reduced_size());
const mfem::Vector displacement =
prepared_operator.GetDisplacementMap().gather(
prepared_test::make_displacement(f, 1.0));
prepared_operator.Prepare(displacement);
const mfem::Vector first = prepared_operator.GetFluxMap().gather(
prepared_test::make_deterministic_vector(f.gravityFluxFes->GetTrueVSize(),
0.17));
const mfem::Vector second = prepared_operator.GetFluxMap().gather(
prepared_test::make_deterministic_vector(f.gravityFluxFes->GetTrueVSize(),
0.83));
const mfem::Vector combination =
prepared_test::linear_combination(first, 1.7, second, -0.4);
mfem::Vector first_action;
mfem::Vector second_action;
mfem::Vector combination_action;
mfem::Vector zero_action;
prepared_operator.Mult(first, first_action);
prepared_operator.Mult(second, second_action);
prepared_operator.Mult(combination, combination_action);
mfem::Vector expected_combination =
prepared_test::linear_combination(first_action, 1.7, second_action, -0.4);
mfem::Vector zero(first.Size());
zero = 0.0;
prepared_operator.Mult(zero, zero_action);
const MPI_Comm communicator = f.gravityFluxFes->GetComm();
const double first_second_product =
prepared_test::global_dot(first, second_action, communicator);
const double second_first_product =
prepared_test::global_dot(second, first_action, communicator);
const double symmetry_error = prepared_test::relative_scalar_error(
first_second_product, second_first_product);
const double linearity_error = prepared_test::relative_error(
combination_action, expected_combination, communicator);
const double first_energy =
prepared_test::global_dot(first, first_action, communicator);
const double second_energy =
prepared_test::global_dot(second, second_action, communicator);
const std::uint64_t preparation_count =
prepared_operator.GetPreparationCount();
mfem::Vector repeated_action;
prepared_operator.Mult(first, repeated_action);
INFO("u^T M v = " << first_second_product);
INFO("v^T M u = " << second_first_product);
INFO("Relative symmetry error = " << symmetry_error);
INFO("Relative linearity error = " << linearity_error);
INFO("u^T M u = " << first_energy);
INFO("v^T M v = " << second_energy);
CHECK_THAT(symmetry_error, WithinAbs(0.0, 2.0e-12));
CHECK_THAT(linearity_error, WithinAbs(0.0, 2.0e-12));
CHECK_THAT(prepared_test::global_norm(zero_action, communicator),
WithinAbs(0.0, 1.0e-14));
CHECK(first_energy > 0.0);
CHECK(second_energy > 0.0);
CHECK(prepared_test::relative_error(repeated_action, first_action,
communicator) < 2.0e-14);
CHECK(prepared_operator.GetPreparationCount() == preparation_count);
}
TEST_CASE("Prepared Mapped Hdiv Mass Diagonal Is Positive Across Both Domains",
tags::gravity_prepared) {
auto args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
operators::PreparedMappedHDivMassOperator prepared_operator(
f, *f.domainMapperStateless);
const mfem::Vector displacement =
prepared_operator.GetDisplacementMap().gather(
prepared_test::make_displacement(f, 1.0));
prepared_operator.Prepare(displacement);
mfem::Vector diagonal;
mfem::Vector true_diagonal;
prepared_operator.AssembleDiagonal(diagonal);
prepared_operator.AssembleTrueDiagonal(true_diagonal);
REQUIRE(diagonal.Size() == prepared_operator.Height());
REQUIRE(true_diagonal.Size() == prepared_operator.GetFluxMap().full_size());
const mfem::Vector gathered_true_diagonal =
prepared_operator.GetFluxMap().gather(true_diagonal);
for (int i = 0; i < diagonal.Size(); ++i) {
REQUIRE(std::isfinite(diagonal(i)));
CHECK(diagonal(i) > 0.0);
CHECK_THAT(diagonal(i), WithinAbs(gathered_true_diagonal(i),
1.0e-14 * std::abs(diagonal(i))));
}
}

View File

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

View File

@@ -138,9 +138,7 @@ TEST_CASE(
mean_field::operators::PreparedHydrostaticEquilibriumOperator preparedOperator(f, *f.domainMapperStateless);
const mfem::Vector enthalpy =
field_dof_test_utils::make_deterministic_supported_vector<mean_field::field::Enthalpy>(
*f.enthalpyFes, 0.34
);
field_dof_test_utils::make_deterministic_supported_vector<mean_field::field::Enthalpy>(*f.enthalpyFes, 0.34);
const mfem::Vector gravityPotential =
field_dof_test_utils::make_deterministic_supported_vector<mean_field::field::Gravity>(
@@ -161,9 +159,7 @@ TEST_CASE(
);
const mfem::Vector enthalpyVariation =
field_dof_test_utils::make_deterministic_supported_vector<mean_field::field::Enthalpy>(
*f.enthalpyFes, 1.07
);
field_dof_test_utils::make_deterministic_supported_vector<mean_field::field::Enthalpy>(*f.enthalpyFes, 1.07);
const mfem::Vector gravityPotentialVariation =
field_dof_test_utils::make_deterministic_supported_vector<mean_field::field::Gravity>(
@@ -236,9 +232,7 @@ TEST_CASE(
mean_field::operators::PreparedHydrostaticEquilibriumOperator preparedOperator(f, *f.domainMapperStateless);
const mfem::Vector enthalpy =
field_dof_test_utils::make_deterministic_supported_vector<mean_field::field::Enthalpy>(
*f.enthalpyFes, 0.41
);
field_dof_test_utils::make_deterministic_supported_vector<mean_field::field::Enthalpy>(*f.enthalpyFes, 0.41);
const mfem::Vector gravityPotential =
field_dof_test_utils::make_deterministic_supported_vector<mean_field::field::Gravity>(
@@ -277,9 +271,7 @@ TEST_CASE(
CHECK(preparedOperator.GetEnthalpyMap().inactive_size() > 0);
const mfem::Vector enthalpyVariation =
field_dof_test_utils::make_deterministic_supported_vector<mean_field::field::Enthalpy>(
*f.enthalpyFes, 1.12
);
field_dof_test_utils::make_deterministic_supported_vector<mean_field::field::Enthalpy>(*f.enthalpyFes, 1.12);
const mfem::Vector gravityPotentialVariation =
field_dof_test_utils::make_deterministic_supported_vector<mean_field::field::Gravity>(

View File

@@ -26,16 +26,13 @@ namespace mass_normalization_test_utils {
);
[[nodiscard]] mean_field::operators::MassNormalizationLayout make_layout(const mean_field::fem::FEM &f) {
const auto densityMap =
field_dof_test_utils::make_map<mean_field::field::Density>(*f.densityFes);
const auto densityMap = field_dof_test_utils::make_map<mean_field::field::Density>(*f.densityFes);
const auto displacementMap =
field_dof_test_utils::make_map<mean_field::field::Displacement>(*f.displacementFes);
const auto gravityFluxMap =
field_dof_test_utils::make_map<mean_field::field::Gravity>(*f.gravityFluxFes);
const auto gravityFluxMap = field_dof_test_utils::make_map<mean_field::field::Gravity>(*f.gravityFluxFes);
const auto gravityPotentialMap =
field_dof_test_utils::make_map<mean_field::field::Gravity>(*f.gravityPotentialFes);
const auto enthalpyMap =
field_dof_test_utils::make_map<mean_field::field::Enthalpy>(*f.enthalpyFes);
const auto enthalpyMap = field_dof_test_utils::make_map<mean_field::field::Enthalpy>(*f.enthalpyFes);
const std::array<int, CoupledForm::value_block_count> valueSizes{
densityMap.reduced_size(), displacementMap.reduced_size(), gravityFluxMap.reduced_size(),
@@ -43,7 +40,7 @@ namespace mass_normalization_test_utils {
};
const std::array<int, CoupledForm::residual_block_count> residualSizes{
gravityFluxMap.reduced_size(), gravityPotentialMap.reduced_size(), densityMap.reduced_size(),
gravityFluxMap.reduced_size(), gravityPotentialMap.reduced_size(), densityMap.reduced_size(),
displacementMap.reduced_size(), enthalpyMap.reduced_size(), 1
};
@@ -345,8 +342,7 @@ TEST_CASE(
mean_field::operators::PreparedMassNormalizationOperator massOperator(f, *f.domainMapperStateless, gravityContext);
massOperator.Prepare({.targetMass = 1.11}, dependencies);
const mfem::Vector reducedDisplacementDirection =
gravityContext.GetDisplacementMap().gather(displacementDirection);
const mfem::Vector reducedDisplacementDirection = gravityContext.GetDisplacementMap().gather(displacementDirection);
mfem::Vector analyticAction;
massOperator.ApplyDisplacementJacobianAction(reducedDisplacementDirection, analyticAction);
@@ -476,9 +472,8 @@ TEST_CASE(
mean_field::operators::PreparedMassNormalizationOperator massOperator(f, *f.domainMapperStateless, gravityContext);
massOperator.Prepare({.targetMass = 1.19}, 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 displacementAction;
@@ -486,9 +481,7 @@ TEST_CASE(
massOperator.ApplyDensityJacobianAction(reducedDensityDirection, densityAction);
massOperator.ApplyDisplacementJacobianAction(reducedDisplacementDirection, displacementAction);
massOperator.ApplyCompleteJacobianAction(
reducedDensityDirection, reducedDisplacementDirection, completeAction
);
massOperator.ApplyCompleteJacobianAction(reducedDensityDirection, reducedDisplacementDirection, completeAction);
CHECK(
mass_normalization_test_utils::relative_error(completeAction(0), densityAction(0) + displacementAction(0)) <
@@ -498,10 +491,7 @@ TEST_CASE(
const auto layout = mass_normalization_test_utils::make_layout(f);
mean_field::operators::PreparedMassNormalizationJacobianOperator adapter(layout, massOperator);
CHECK(
layout.size(mass_normalization_test_utils::densityValue) ==
gravityContext.GetDensityMap().reduced_size()
);
CHECK(layout.size(mass_normalization_test_utils::densityValue) == gravityContext.GetDensityMap().reduced_size());
CHECK(
layout.size(mass_normalization_test_utils::displacementValue) ==
gravityContext.GetDisplacementMap().reduced_size()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -8,115 +8,202 @@
import mean_field;
import test_helpers;
TEST_CASE("Polytropic EOS Satisfies Its Analytic Identities",
tags::barotrope_eos_unit) {
constexpr double polytropic_index = 3.0;
constexpr double polytropic_constant = 1.5;
TEST_CASE(
"Polytropic EOS Satisfies Its Analytic Identities",
tags::barotrope_eos_unit
) {
using namespace mean_field::eos;
const mean_field::eos::Polytrope barotrope(polytropic_index,
polytropic_constant);
constexpr double polytropic_index = 3.0;
constexpr double polytropic_constant = 1.5;
const std::array<double, 5> densities{1.0e-6, 1.0e-3, 0.1, 0.7, 2.0};
const Polytrope barotrope(polytropic_index, polytropic_constant);
for (const double density : densities) {
const double pressure = barotrope.pressure_from_density(density);
using densityV = DensityValue;
using pressureV = PressureValue;
using enthalpyV = SpecificEnthalpyValue;
const double enthalpy = barotrope.enthalpy_from_density(density);
constexpr std::array<densityV, 5> densities{
densityV{1.0e-6}, densityV{1.0e-3}, densityV{0.1}, densityV{0.7}, densityV{2.0}
};
const double reconstructed_density =
barotrope.density_from_enthalpy(enthalpy);
for (const densityV density : densities) {
const pressureV pressure = evaluate<quantity::Pressure>(barotrope, density);
const double reconstructed_pressure =
barotrope.pressure_from_enthalpy(enthalpy);
const enthalpyV enthalpy = evaluate<quantity::SpecificEnthalpy>(barotrope, density);
const densityV reconstructed_density = evaluate<quantity::Density>(barotrope, enthalpy);
const double reconstructed_enthalpy =
barotrope.enthalpy_from_pressure(pressure);
const pressureV reconstructed_pressure = evaluate<quantity::Pressure>(barotrope, enthalpy);
CHECK_THAT(reconstructed_density,
Catch::Matchers::WithinRel(density, 2.0e-14));
const enthalpyV reconstructed_enthalpy = evaluate<quantity::SpecificEnthalpy>(barotrope, pressure);
CHECK_THAT(reconstructed_pressure,
Catch::Matchers::WithinRel(pressure, 2.0e-14));
CHECK_THAT(reconstructed_density.value(), Catch::Matchers::WithinRel(density.value(), 2.0e-14));
CHECK_THAT(reconstructed_enthalpy,
Catch::Matchers::WithinRel(enthalpy, 2.0e-14));
CHECK_THAT(reconstructed_pressure.value(), Catch::Matchers::WithinRel(pressure.value(), 2.0e-14));
CHECK_THAT(pressure,
Catch::Matchers::WithinRel(
density * enthalpy / (polytropic_index + 1.0), 2.0e-14));
CHECK_THAT(reconstructed_enthalpy.value(), Catch::Matchers::WithinRel(enthalpy.value(), 2.0e-14));
CHECK_THAT(barotrope.pressure_derivative_from_enthalpy(enthalpy),
Catch::Matchers::WithinRel(density, 2.0e-14));
CHECK_THAT(
pressure.value(),
Catch::Matchers::WithinRel(density.value() * enthalpy.value() / (polytropic_index + 1.0), 2.0e-14)
);
CHECK_THAT(
barotrope.pressure_derivative_from_density(density),
Catch::Matchers::WithinRel(enthalpy / polytropic_index, 2.0e-14));
}
CHECK_THAT(
(mean_field::eos::partialDerivative<
mean_field::eos::quantity::Pressure, mean_field::eos::quantity::SpecificEnthalpy>(
barotrope, mean_field::eos::SpecificEnthalpyValue{enthalpy}
)
.value()),
Catch::Matchers::WithinRel(density.value(), 2.0e-14)
);
CHECK_THAT(
(mean_field::eos::partialDerivative<
mean_field::eos::quantity::Pressure, mean_field::eos::quantity::Density>(
barotrope, mean_field::eos::DensityValue{density}
)
.value()),
Catch::Matchers::WithinRel(enthalpy.value() / polytropic_index, 2.0e-14)
);
}
}
TEST_CASE("Polytropic EOS Derivatives Match Centered Differences",
tags::barotrope_eos_jacobian) {
const mean_field::eos::Polytrope barotrope(3.0, 1.5);
TEST_CASE(
"Polytropic EOS Derivatives Match Centered Differences",
tags::barotrope_eos_jacobian
) {
using namespace mean_field::eos;
const std::array<double, 4> enthalpies{0.05, 0.2, 0.7, 1.4};
const Polytrope barotrope(3.0, 1.5);
for (const double enthalpy : enthalpies) {
const double step = 1.0e-6 * std::max(1.0, enthalpy);
using densityV = DensityValue;
using pressureV = PressureValue;
using enthalpyV = SpecificEnthalpyValue;
const double density_difference =
(barotrope.density_from_enthalpy(enthalpy + step) -
barotrope.density_from_enthalpy(enthalpy - step)) /
(2.0 * step);
constexpr std::array<enthalpyV, 4> enthalpies{enthalpyV{0.05}, enthalpyV{0.2}, enthalpyV{0.7}, enthalpyV{1.4}};
const double pressure_difference =
(barotrope.pressure_from_enthalpy(enthalpy + step) -
barotrope.pressure_from_enthalpy(enthalpy - step)) /
(2.0 * step);
for (const enthalpyV enthalpy : enthalpies) {
const enthalpyV step = enthalpyV{1.0e-6} * std::max(1.0, enthalpy.value());
CHECK_THAT(
density_difference,
Catch::Matchers::WithinRel(
barotrope.density_derivative_from_enthalpy(enthalpy), 5.0e-10));
const densityV density_difference = (evaluate<quantity::Density>(barotrope, enthalpy + step) -
evaluate<quantity::Density>(barotrope, enthalpy - step)) /
(2.0 * step.value());
CHECK_THAT(
pressure_difference,
Catch::Matchers::WithinRel(
barotrope.pressure_derivative_from_enthalpy(enthalpy), 5.0e-10));
}
const pressureV pressure_difference = (evaluate<quantity::Pressure>(barotrope, enthalpy + step) -
evaluate<quantity::Pressure>(barotrope, enthalpy - step)) /
(2.0 * step.value());
CHECK_THAT(
density_difference.value(),
Catch::Matchers::WithinRel(
mean_field::eos::partialDerivative<
mean_field::eos::quantity::Density, mean_field::eos::quantity::SpecificEnthalpy>(
barotrope, enthalpy
)
.value(),
5.0e-10
)
);
CHECK_THAT(
pressure_difference.value(),
Catch::Matchers::WithinRel(
mean_field::eos::partialDerivative<
mean_field::eos::quantity::Pressure, mean_field::eos::quantity::SpecificEnthalpy>(
barotrope, enthalpy
)
.value(),
5.0e-10
)
);
}
}
TEST_CASE("Polytropic EOS Has An Exact Zero Density Surface",
tags::barotrope_eos_unit) {
const mean_field::eos::Polytrope barotrope(3.0, 1.5);
TEST_CASE(
"Polytropic EOS Has An Exact Zero Density Surface",
tags::barotrope_eos_unit
) {
const mean_field::eos::Polytrope barotrope(3.0, 1.5);
CHECK(barotrope.density_from_enthalpy(-1.0) == 0.0);
CHECK(barotrope.density_from_enthalpy(0.0) == 0.0);
CHECK(
mean_field::eos::evaluate<mean_field::eos::quantity::Density>(
barotrope, mean_field::eos::SpecificEnthalpyValue{-1.0}
)
.value() == 0.0
);
CHECK(
mean_field::eos::evaluate<mean_field::eos::quantity::Density>(
barotrope, mean_field::eos::SpecificEnthalpyValue{0.0}
)
.value() == 0.0
);
CHECK(barotrope.pressure_from_enthalpy(-1.0) == 0.0);
CHECK(barotrope.pressure_from_enthalpy(0.0) == 0.0);
CHECK(
mean_field::eos::evaluate<mean_field::eos::quantity::Pressure>(
barotrope, mean_field::eos::SpecificEnthalpyValue{-1.0}
)
.value() == 0.0
);
CHECK(
mean_field::eos::evaluate<mean_field::eos::quantity::Pressure>(
barotrope, mean_field::eos::SpecificEnthalpyValue{0.0}
)
.value() == 0.0
);
CHECK(barotrope.density_derivative_from_enthalpy(-1.0) == 0.0);
CHECK(
(mean_field::eos::partialDerivative<
mean_field::eos::quantity::Density, mean_field::eos::quantity::SpecificEnthalpy>(
barotrope, mean_field::eos::SpecificEnthalpyValue{-1.0}
)
.value() == 0.0)
);
CHECK(barotrope.density_derivative_from_enthalpy(0.0) == 0.0);
CHECK(
(mean_field::eos::partialDerivative<
mean_field::eos::quantity::Density, mean_field::eos::quantity::SpecificEnthalpy>(
barotrope, mean_field::eos::SpecificEnthalpyValue{0.0}
)
.value() == 0.0)
);
CHECK(barotrope.pressure_derivative_from_enthalpy(0.0) == 0.0);
CHECK(
(mean_field::eos::partialDerivative<
mean_field::eos::quantity::Pressure, mean_field::eos::quantity::SpecificEnthalpy>(
barotrope, mean_field::eos::SpecificEnthalpyValue{0.0}
)
.value() == 0.0)
);
}
TEST_CASE("Polytropic EOS Rejects Invalid Material Parameters",
tags::barotrope_eos_unit) {
CHECK_THROWS_AS(mean_field::eos::Polytrope(0.5, 1.0), std::invalid_argument);
TEST_CASE(
"Polytropic EOS Rejects Invalid Material Parameters",
tags::barotrope_eos_unit
) {
CHECK_THROWS_AS(mean_field::eos::Polytrope(0.5, 1.0), std::invalid_argument);
CHECK_THROWS_AS(mean_field::eos::Polytrope(3.0, 0.0), std::invalid_argument);
CHECK_THROWS_AS(mean_field::eos::Polytrope(3.0, 0.0), std::invalid_argument);
CHECK_THROWS_AS(
mean_field::eos::Polytrope(std::numeric_limits<double>::infinity(), 1.0),
std::invalid_argument);
CHECK_THROWS_AS(mean_field::eos::Polytrope(std::numeric_limits<double>::infinity(), 1.0), std::invalid_argument);
const mean_field::eos::Polytrope barotrope(3.0, 1.0);
const mean_field::eos::Polytrope barotrope(3.0, 1.0);
CHECK_THROWS_AS(barotrope.pressure_from_density(-1.0), std::domain_error);
CHECK_THROWS_AS(
mean_field::eos::evaluate<mean_field::eos::quantity::Pressure>(barotrope, mean_field::eos::DensityValue{-1.0}),
std::domain_error
);
CHECK_THROWS_AS(barotrope.enthalpy_from_density(-1.0), std::domain_error);
CHECK_THROWS_AS(
mean_field::eos::evaluate<mean_field::eos::quantity::SpecificEnthalpy>(
barotrope, mean_field::eos::DensityValue{-1.0}
),
std::domain_error
);
CHECK_THROWS_AS(barotrope.enthalpy_from_pressure(-1.0), std::domain_error);
CHECK_THROWS_AS(
mean_field::eos::evaluate<mean_field::eos::quantity::SpecificEnthalpy>(
barotrope, mean_field::eos::PressureValue{-1.0}
),
std::domain_error
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,125 @@
#include <catch2/catch_test_macros.hpp>
import mean_field;
import test_helpers;
namespace {
namespace eos = mean_field::eos;
class DensityClosureEquationOfState final {
public:
using Relations = eos::RelationCatalog<eos::DensityFromSpecificEnthalpy>;
[[nodiscard]] constexpr eos::DensityValue evaluate(
eos::DensityFromSpecificEnthalpy,
const eos::SpecificEnthalpyValue specificEnthalpy
) const noexcept {
return eos::DensityValue{specificEnthalpy.value()};
}
[[nodiscard]] constexpr eos::PartialDerivative<
eos::quantity::Density,
eos::quantity::SpecificEnthalpy>
partialDerivative(
eos::DensityFromSpecificEnthalpy,
eos::WithRespectTo<eos::quantity::SpecificEnthalpy>,
eos::SpecificEnthalpyValue
) const noexcept {
return eos::PartialDerivative<eos::quantity::Density, eos::quantity::SpecificEnthalpy>{1.0};
}
};
class DensityClosureWithoutDerivative final {
public:
using Relations = eos::RelationCatalog<eos::DensityFromSpecificEnthalpy>;
[[nodiscard]] constexpr eos::DensityValue evaluate(
eos::DensityFromSpecificEnthalpy,
const eos::SpecificEnthalpyValue specificEnthalpy
) const noexcept {
return eos::DensityValue{specificEnthalpy.value()};
}
};
class EnthalpyPressureEquationOfState final {
public:
using Relations = eos::RelationCatalog<eos::PressureFromSpecificEnthalpy>;
[[nodiscard]] constexpr eos::PressureValue evaluate(
eos::PressureFromSpecificEnthalpy,
const eos::SpecificEnthalpyValue specificEnthalpy
) const noexcept {
return eos::PressureValue{2.0 * specificEnthalpy.value()};
}
[[nodiscard]] constexpr eos::PartialDerivative<
eos::quantity::Pressure,
eos::quantity::SpecificEnthalpy>
partialDerivative(
eos::PressureFromSpecificEnthalpy,
eos::WithRespectTo<eos::quantity::SpecificEnthalpy>,
eos::SpecificEnthalpyValue
) const noexcept {
return eos::PartialDerivative<eos::quantity::Pressure, eos::quantity::SpecificEnthalpy>{2.0};
}
};
class DensitySeedEquationOfState final {
public:
using Relations = eos::RelationCatalog<eos::SpecificEnthalpyFromDensity>;
[[nodiscard]] constexpr eos::SpecificEnthalpyValue evaluate(
eos::SpecificEnthalpyFromDensity,
const eos::DensityValue density
) const noexcept {
return eos::SpecificEnthalpyValue{3.0 * density.value()};
}
};
class GeneralEquationOfStateWithoutCurrentConsumerRelations final {
public:
using Relations = eos::RelationCatalog<eos::SpecificEnthalpyFromPressure>;
[[nodiscard]] constexpr eos::SpecificEnthalpyValue evaluate(
eos::SpecificEnthalpyFromPressure,
const eos::PressureValue pressure
) const noexcept {
return eos::SpecificEnthalpyValue{pressure.value()};
}
};
} // namespace
TEST_CASE(
"Barotropic Closure EOS Requires Density And Its Enthalpy Derivative",
tags::barotropic_closure_equation_of_state_contract
) {
STATIC_CHECK(eos::BarotropicClosureEquationOfState<eos::Polytrope>);
STATIC_CHECK(eos::BarotropicClosureEquationOfState<DensityClosureEquationOfState>);
STATIC_CHECK(eos::EquationOfStateModel<DensityClosureWithoutDerivative>);
STATIC_CHECK_FALSE(eos::BarotropicClosureEquationOfState<DensityClosureWithoutDerivative>);
STATIC_CHECK_FALSE(eos::BarotropicClosureEquationOfState<EnthalpyPressureEquationOfState>);
}
TEST_CASE(
"Pressure Force EOS Requires Pressure And Its Enthalpy Derivative",
tags::pressure_force_equation_of_state_contract
) {
STATIC_CHECK(eos::PressureForceEquationOfState<eos::Polytrope>);
STATIC_CHECK(eos::PressureForceEquationOfState<EnthalpyPressureEquationOfState>);
STATIC_CHECK_FALSE(eos::PressureForceEquationOfState<DensityClosureEquationOfState>);
STATIC_CHECK_FALSE(eos::PressureForceEquationOfState<DensityClosureWithoutDerivative>);
}
TEST_CASE(
"Structure Seed EOS Requires Enthalpy From Density",
tags::structure_seed_equation_of_state_contract
) {
STATIC_CHECK(eos::StructureSeedEquationOfState<eos::Polytrope>);
STATIC_CHECK(eos::StructureSeedEquationOfState<DensitySeedEquationOfState>);
STATIC_CHECK_FALSE(eos::StructureSeedEquationOfState<DensityClosureEquationOfState>);
STATIC_CHECK(eos::EquationOfStateModel<GeneralEquationOfStateWithoutCurrentConsumerRelations>);
STATIC_CHECK_FALSE(eos::StructureSeedEquationOfState<GeneralEquationOfStateWithoutCurrentConsumerRelations>);
STATIC_CHECK_FALSE(eos::BarotropicClosureEquationOfState<GeneralEquationOfStateWithoutCurrentConsumerRelations>);
STATIC_CHECK_FALSE(eos::PressureForceEquationOfState<GeneralEquationOfStateWithoutCurrentConsumerRelations>);
}

View File

@@ -0,0 +1,322 @@
#include <array>
#include <concepts>
#include <expected>
#include <limits>
#include <memory>
#include <span>
#include <string_view>
#include <type_traits>
#include <catch2/catch_test_macros.hpp>
import mean_field;
import test_helpers;
namespace {
namespace eos = mean_field::eos;
class LinearPressureEquationOfState final {
public:
using Relations = eos::RelationCatalog<eos::PressureFromDensity>;
[[nodiscard]] constexpr eos::PressureValue evaluate(
eos::PressureFromDensity,
const eos::DensityValue density
) const noexcept {
return eos::PressureValue{2.0 * density.value() + 0.5};
}
};
struct DensityAlias final : eos::ThermodynamicQuantity {
static constexpr std::string_view identifier = "density";
};
class AmbiguouslyIdentifiedEquationOfState final {
public:
using Relations = eos::RelationCatalog<eos::Relation<DensityAlias, eos::quantity::Density>>;
[[nodiscard]] constexpr eos::QuantityValue<DensityAlias> evaluate(
eos::Relation<
DensityAlias,
eos::quantity::Density>,
const eos::DensityValue density
) const noexcept {
return eos::QuantityValue<DensityAlias>{density.value()};
}
};
[[nodiscard]] std::expected<
eos::PressureValue,
eos::EvaluationError>
pressureAtDensity(
const eos::EquationOfStateView equationOfState,
const eos::DensityValue density
) {
return equationOfState.tryEvaluate<eos::quantity::Pressure>(density);
}
[[nodiscard]] const eos::RuntimeRelationDescriptor *findRelation(
const eos::EquationOfStateView equationOfState,
const eos::ThermodynamicQuantityId output,
const eos::ThermodynamicQuantityId input
) {
for (const eos::RuntimeRelationDescriptor &relation : equationOfState.relations()) {
if (relation.outputQuantity == output && relation.inputQuantities.size() == 1 &&
relation.inputQuantities[0] == input) {
return std::addressof(relation);
}
}
return nullptr;
}
} // namespace
TEST_CASE(
"Runtime EOS View Generates The Polytropic Relation Catalog",
tags::equation_of_state_runtime_contract
) {
STATIC_CHECK(eos::RuntimeEquationOfStateModel<eos::Polytrope>);
STATIC_CHECK(eos::RuntimeEquationOfStateModel<LinearPressureEquationOfState>);
STATIC_CHECK(eos::EquationOfStateModel<AmbiguouslyIdentifiedEquationOfState>);
STATIC_CHECK_FALSE(eos::RuntimeEquationOfStateModel<AmbiguouslyIdentifiedEquationOfState>);
STATIC_CHECK(std::is_trivially_copyable_v<eos::EquationOfStateView>);
STATIC_CHECK_FALSE(std::constructible_from<eos::EquationOfStateView, eos::Polytrope &&>);
const eos::Polytrope equationOfState(3.0, 0.25);
const eos::Polytrope secondEquationOfState(1.5, 0.73);
const eos::EquationOfStateView view{equationOfState};
const eos::EquationOfStateView secondView{secondEquationOfState};
REQUIRE(view.relations().size() == eos::Polytrope::Relations::size);
CHECK(view.relations().data() == secondView.relations().data());
CHECK(eos::thermodynamicQuantityId<eos::quantity::Density>.name() == "density");
CHECK(eos::thermodynamicQuantityId<eos::quantity::Pressure>.name() == "pressure");
CHECK(eos::thermodynamicQuantityId<eos::quantity::SpecificEnthalpy>.name() == "specific_enthalpy");
const eos::RuntimeRelationDescriptor *pressureFromDensity = findRelation(
view, eos::thermodynamicQuantityId<eos::quantity::Pressure>,
eos::thermodynamicQuantityId<eos::quantity::Density>
);
REQUIRE(pressureFromDensity != nullptr);
CHECK(pressureFromDensity->hasPartialDerivative(0));
const eos::RuntimeRelationDescriptor *specificEnthalpyFromPressure = findRelation(
view, eos::thermodynamicQuantityId<eos::quantity::SpecificEnthalpy>,
eos::thermodynamicQuantityId<eos::quantity::Pressure>
);
REQUIRE(specificEnthalpyFromPressure != nullptr);
CHECK_FALSE(specificEnthalpyFromPressure->hasPartialDerivative(0));
const eos::RuntimeRelationDescriptor *pressureFromSpecificEnthalpy = findRelation(
view, eos::thermodynamicQuantityId<eos::quantity::Pressure>,
eos::thermodynamicQuantityId<eos::quantity::SpecificEnthalpy>
);
REQUIRE(pressureFromSpecificEnthalpy != nullptr);
CHECK(pressureFromSpecificEnthalpy->hasPartialDerivative(0));
const eos::RuntimeRelationDescriptor *specificEnthalpyFromDensity = findRelation(
view, eos::thermodynamicQuantityId<eos::quantity::SpecificEnthalpy>,
eos::thermodynamicQuantityId<eos::quantity::Density>
);
REQUIRE(specificEnthalpyFromDensity != nullptr);
CHECK_FALSE(specificEnthalpyFromDensity->hasPartialDerivative(0));
const eos::RuntimeRelationDescriptor *densityFromSpecificEnthalpy = findRelation(
view, eos::thermodynamicQuantityId<eos::quantity::Density>,
eos::thermodynamicQuantityId<eos::quantity::SpecificEnthalpy>
);
REQUIRE(densityFromSpecificEnthalpy != nullptr);
CHECK(densityFromSpecificEnthalpy->hasPartialDerivative(0));
}
TEST_CASE(
"Runtime EOS View Matches Typed Polytropic Evaluation",
tags::equation_of_state_runtime_compatibility
) {
const eos::Polytrope equationOfState(3.0, 0.25);
const eos::EquationOfStateView view{equationOfState};
const eos::DensityValue density{0.7};
const eos::SpecificEnthalpyValue specificEnthalpy{0.9};
const eos::PressureValue pressure{0.04};
const auto runtimePressureFromDensity = view.tryEvaluate<eos::quantity::Pressure>(density);
const auto runtimePressureFromSpecificEnthalpy = view.tryEvaluate<eos::quantity::Pressure>(specificEnthalpy);
const auto runtimeSpecificEnthalpyFromDensity = view.tryEvaluate<eos::quantity::SpecificEnthalpy>(density);
const auto runtimeSpecificEnthalpyFromPressure = view.tryEvaluate<eos::quantity::SpecificEnthalpy>(pressure);
const auto runtimeDensityFromSpecificEnthalpy = view.tryEvaluate<eos::quantity::Density>(specificEnthalpy);
REQUIRE(runtimePressureFromDensity.has_value());
REQUIRE(runtimePressureFromSpecificEnthalpy.has_value());
REQUIRE(runtimeSpecificEnthalpyFromDensity.has_value());
REQUIRE(runtimeSpecificEnthalpyFromPressure.has_value());
REQUIRE(runtimeDensityFromSpecificEnthalpy.has_value());
CHECK(
runtimePressureFromDensity->value() == eos::evaluate<eos::quantity::Pressure>(equationOfState, density).value()
);
CHECK(
runtimePressureFromSpecificEnthalpy->value() ==
eos::evaluate<eos::quantity::Pressure>(equationOfState, specificEnthalpy).value()
);
CHECK(
runtimeSpecificEnthalpyFromDensity->value() ==
eos::evaluate<eos::quantity::SpecificEnthalpy>(equationOfState, density).value()
);
CHECK(
runtimeSpecificEnthalpyFromPressure->value() ==
eos::evaluate<eos::quantity::SpecificEnthalpy>(equationOfState, pressure).value()
);
CHECK(
runtimeDensityFromSpecificEnthalpy->value() ==
eos::evaluate<eos::quantity::Density>(equationOfState, specificEnthalpy).value()
);
const std::array runtimeDensityInput{
eos::RuntimeQuantityValue{eos::thermodynamicQuantityId<eos::quantity::Density>, density.value()}
};
const auto erasedPressureFromDensity = view.tryEvaluate(
eos::thermodynamicQuantityId<eos::quantity::Pressure>,
std::span<const eos::RuntimeQuantityValue>{runtimeDensityInput}
);
REQUIRE(erasedPressureFromDensity.has_value());
CHECK(erasedPressureFromDensity->quantity == eos::thermodynamicQuantityId<eos::quantity::Pressure>);
CHECK(erasedPressureFromDensity->value == runtimePressureFromDensity->value());
const auto runtimePressureDerivative =
view.tryPartialDerivative<eos::quantity::Pressure, eos::quantity::SpecificEnthalpy>(specificEnthalpy);
const auto runtimeDensityDerivative =
view.tryPartialDerivative<eos::quantity::Density, eos::quantity::SpecificEnthalpy>(specificEnthalpy);
const auto runtimePressureDensityDerivative =
view.tryPartialDerivative<eos::quantity::Pressure, eos::quantity::Density>(density);
REQUIRE(runtimePressureDerivative.has_value());
REQUIRE(runtimeDensityDerivative.has_value());
REQUIRE(runtimePressureDensityDerivative.has_value());
CHECK(
runtimePressureDerivative->value() ==
eos::partialDerivative<eos::quantity::Pressure, eos::quantity::SpecificEnthalpy>(
equationOfState, specificEnthalpy
)
.value()
);
CHECK(
runtimeDensityDerivative->value() ==
eos::partialDerivative<eos::quantity::Density, eos::quantity::SpecificEnthalpy>(
equationOfState, specificEnthalpy
)
.value()
);
CHECK(
runtimePressureDensityDerivative->value() ==
eos::partialDerivative<eos::quantity::Pressure, eos::quantity::Density>(equationOfState, density).value()
);
const auto erasedPressureDensityDerivative = view.tryPartialDerivative(
eos::thermodynamicQuantityId<eos::quantity::Pressure>, eos::thermodynamicQuantityId<eos::quantity::Density>,
std::span<const eos::RuntimeQuantityValue>{runtimeDensityInput}
);
REQUIRE(erasedPressureDensityDerivative.has_value());
CHECK(*erasedPressureDensityDerivative == runtimePressureDensityDerivative->value());
}
TEST_CASE(
"Runtime EOS View Reports Unsupported And Invalid Requests",
tags::equation_of_state_runtime_contract
) {
const eos::Polytrope equationOfState(3.0, 0.25);
const eos::EquationOfStateView view{equationOfState};
constexpr eos::ThermodynamicQuantityId temperature{"temperature"};
const std::array densityInput{eos::RuntimeQuantityValue{eos::thermodynamicQuantityId<eos::quantity::Density>, 0.7}};
const std::array pressureInput{
eos::RuntimeQuantityValue{eos::thermodynamicQuantityId<eos::quantity::Pressure>, 0.04}
};
const std::array<eos::RuntimeQuantityValue, 0> noInputs{};
const auto unsupportedOutput =
view.tryEvaluate(temperature, std::span<const eos::RuntimeQuantityValue>{densityInput});
REQUIRE_FALSE(unsupportedOutput.has_value());
CHECK(unsupportedOutput.error().code() == eos::EvaluationErrorCode::unsupported_relation);
const auto wrongInputCount = view.tryEvaluate(
eos::thermodynamicQuantityId<eos::quantity::Pressure>, std::span<const eos::RuntimeQuantityValue>{noInputs}
);
REQUIRE_FALSE(wrongInputCount.has_value());
CHECK(wrongInputCount.error().code() == eos::EvaluationErrorCode::wrong_input_count);
const auto wrongInputQuantity = view.tryEvaluate(
eos::thermodynamicQuantityId<eos::quantity::Density>, std::span<const eos::RuntimeQuantityValue>{pressureInput}
);
REQUIRE_FALSE(wrongInputQuantity.has_value());
CHECK(wrongInputQuantity.error().code() == eos::EvaluationErrorCode::wrong_input_quantity);
const auto unsupportedDerivative = view.tryPartialDerivative(
eos::thermodynamicQuantityId<eos::quantity::SpecificEnthalpy>,
eos::thermodynamicQuantityId<eos::quantity::Pressure>, std::span<const eos::RuntimeQuantityValue>{pressureInput}
);
REQUIRE_FALSE(unsupportedDerivative.has_value());
CHECK(unsupportedDerivative.error().code() == eos::EvaluationErrorCode::unsupported_derivative);
const auto invalidDensity = view.tryEvaluate<eos::quantity::Pressure>(eos::DensityValue{-0.1});
REQUIRE_FALSE(invalidDensity.has_value());
CHECK(invalidDensity.error().code() == eos::EvaluationErrorCode::outside_domain);
const auto nonfiniteDensity =
view.tryEvaluate<eos::quantity::Pressure>(eos::DensityValue{std::numeric_limits<double>::quiet_NaN()});
REQUIRE_FALSE(nonfiniteDensity.has_value());
CHECK(nonfiniteDensity.error().code() == eos::EvaluationErrorCode::nonfinite_input);
}
TEST_CASE(
"One Runtime EOS Function Accepts Heterogeneous Concrete Models",
tags::equation_of_state_runtime_compatibility
) {
const eos::Polytrope polytrope(3.0, 0.25);
const LinearPressureEquationOfState linearEquationOfState;
const std::array views{eos::EquationOfStateView{polytrope}, eos::EquationOfStateView{linearEquationOfState}};
const eos::DensityValue density{0.7};
const auto polytropicPressure = pressureAtDensity(views[0], density);
const auto linearPressure = pressureAtDensity(views[1], density);
REQUIRE(polytropicPressure.has_value());
REQUIRE(linearPressure.has_value());
CHECK(polytropicPressure->value() == eos::evaluate<eos::quantity::Pressure>(polytrope, density).value());
CHECK(linearPressure->value() == 1.9);
}
TEST_CASE(
"Runtime EOS View Remains Valid When Stable Ownership Moves",
tags::equation_of_state_runtime_contract
) {
auto owner = std::make_unique<const eos::Polytrope>(3.0, 0.25);
const eos::EquationOfStateView view{*owner};
auto movedOwner = std::move(owner);
const auto pressure = view.tryEvaluate<eos::quantity::Pressure>(eos::DensityValue{0.7});
REQUIRE(movedOwner != nullptr);
REQUIRE(pressure.has_value());
CHECK(pressure->value() == eos::evaluate<eos::quantity::Pressure>(*movedOwner, eos::DensityValue{0.7}).value());
}

View File

@@ -0,0 +1,228 @@
#include <concepts>
#include <string_view>
#include <type_traits>
#include <catch2/catch_test_macros.hpp>
import mean_field;
import test_helpers;
namespace {
namespace eos = mean_field::eos;
struct Entropy final : eos::ThermodynamicQuantity { };
struct ElectronFraction final : eos::ThermodynamicQuantity { };
using SpecificEnthalpyFromPressureAndEntropy =
eos::Relation<eos::quantity::SpecificEnthalpy, eos::quantity::Pressure, Entropy>;
class CompleteEquationOfState final {
public:
using Relations = eos::RelationCatalog<
eos::PressureFromDensity,
eos::SpecificEnthalpyFromPressure,
SpecificEnthalpyFromPressureAndEntropy>;
[[nodiscard]] constexpr eos::PressureValue evaluate(
eos::PressureFromDensity,
const eos::DensityValue density
) const noexcept {
return eos::PressureValue{2.0 * density.value()};
}
[[nodiscard]] constexpr eos::SpecificEnthalpyValue evaluate(
eos::SpecificEnthalpyFromPressure,
const eos::PressureValue pressure
) const noexcept {
return eos::SpecificEnthalpyValue{3.0 * pressure.value()};
}
[[nodiscard]] constexpr eos::SpecificEnthalpyValue evaluate(
SpecificEnthalpyFromPressureAndEntropy,
const eos::PressureValue pressure,
const eos::QuantityValue<Entropy> entropy
) const noexcept {
return eos::SpecificEnthalpyValue{3.0 * pressure.value() + 5.0 * entropy.value()};
}
[[nodiscard]] constexpr eos::PartialDerivative<
eos::quantity::SpecificEnthalpy,
Entropy>
partialDerivative(
SpecificEnthalpyFromPressureAndEntropy,
eos::WithRespectTo<Entropy>,
eos::PressureValue,
eos::QuantityValue<Entropy>
) const noexcept {
return eos::PartialDerivative<eos::quantity::SpecificEnthalpy, Entropy>{5.0};
}
};
class MissingRelationImplementation final {
public:
using Relations = eos::RelationCatalog<eos::PressureFromDensity, eos::SpecificEnthalpyFromPressure>;
[[nodiscard]] eos::PressureValue evaluate(
eos::PressureFromDensity,
eos::DensityValue density
) const {
return eos::PressureValue{density.value()};
}
};
class IncorrectRelationOutput final {
public:
using Relations = eos::RelationCatalog<eos::PressureFromDensity>;
[[nodiscard]] eos::DensityValue evaluate(
eos::PressureFromDensity,
eos::DensityValue density
) const {
return density;
}
};
class InvalidRelationCatalog final {
public:
using Relations = eos::RelationCatalog<eos::Relation<double, eos::quantity::Density>>;
};
template <typename EquationOfState>
concept CanEvaluateDensityFromSpecificEnthalpy = requires(const EquationOfState &equationOfState) {
eos::evaluate<eos::quantity::Density>(equationOfState, eos::SpecificEnthalpyValue{1.0});
};
} // namespace
TEST_CASE(
"Thermodynamic Values Preserve Physical Quantity Types",
tags::equation_of_state_quantity_types
) {
STATIC_CHECK(eos::ThermodynamicQuantityType<eos::quantity::Density>);
STATIC_CHECK(eos::ThermodynamicQuantityType<eos::quantity::Pressure>);
STATIC_CHECK(eos::ThermodynamicQuantityType<eos::quantity::SpecificEnthalpy>);
STATIC_CHECK_FALSE(eos::ThermodynamicQuantityType<const eos::quantity::Pressure>);
STATIC_CHECK_FALSE(std::same_as<eos::DensityValue, eos::PressureValue>);
STATIC_CHECK_FALSE(std::same_as<eos::PressureValue, eos::SpecificEnthalpyValue>);
STATIC_CHECK_FALSE(std::is_convertible_v<double, eos::PressureValue>);
STATIC_CHECK_FALSE(std::is_constructible_v<eos::PressureValue, eos::DensityValue>);
STATIC_CHECK(std::is_trivially_copyable_v<eos::DensityValue>);
STATIC_CHECK(std::is_standard_layout_v<eos::DensityValue>);
STATIC_CHECK(sizeof(eos::DensityValue) == sizeof(double));
STATIC_CHECK(sizeof(eos::PressureValue) == sizeof(double));
STATIC_CHECK(sizeof(eos::SpecificEnthalpyValue) == sizeof(double));
STATIC_CHECK(std::is_empty_v<eos::PressureFromDensity>);
constexpr eos::DensityValue density{-0.25};
STATIC_CHECK(density.value() == -0.25);
}
TEST_CASE(
"Thermodynamic Derivatives Preserve Numerator And Denominator Types",
tags::equation_of_state_quantity_types
) {
using PressureByDensity = eos::PartialDerivative<eos::quantity::Pressure, eos::quantity::Density>;
using PressureBySpecificEnthalpy = eos::PartialDerivative<eos::quantity::Pressure, eos::quantity::SpecificEnthalpy>;
STATIC_CHECK_FALSE(std::same_as<PressureByDensity, PressureBySpecificEnthalpy>);
STATIC_CHECK_FALSE(std::is_convertible_v<PressureByDensity, PressureBySpecificEnthalpy>);
STATIC_CHECK(std::is_trivially_copyable_v<PressureByDensity>);
STATIC_CHECK(std::is_standard_layout_v<PressureByDensity>);
STATIC_CHECK(sizeof(PressureByDensity) == sizeof(double));
constexpr PressureByDensity derivative{1.75};
STATIC_CHECK(derivative.value() == 1.75);
}
TEST_CASE(
"EOS Relation Catalog Rejects Invalid And Duplicate Relations",
tags::equation_of_state_relation_contract
) {
using ValidCatalog = eos::RelationCatalog<eos::PressureFromDensity, eos::SpecificEnthalpyFromPressure>;
using DuplicateCatalog = eos::RelationCatalog<eos::PressureFromDensity, eos::PressureFromDensity>;
using InvalidRelation = eos::Relation<double, eos::quantity::Density>;
using InvalidCatalog = eos::RelationCatalog<InvalidRelation>;
using RepeatedInputRelation =
eos::Relation<eos::quantity::Pressure, eos::quantity::Density, eos::quantity::Density>;
using RepeatedInputCatalog = eos::RelationCatalog<RepeatedInputRelation>;
STATIC_CHECK(eos::ValidRelationCatalog<ValidCatalog>);
STATIC_CHECK_FALSE(eos::ValidRelationCatalog<DuplicateCatalog>);
STATIC_CHECK_FALSE(eos::ValidRelationCatalog<InvalidCatalog>);
STATIC_CHECK_FALSE(eos::ValidRelationCatalog<RepeatedInputCatalog>);
STATIC_CHECK_FALSE(eos::ValidRelationCatalog<eos::RelationCatalog<>>);
STATIC_CHECK(eos::relationCatalogContains<ValidCatalog, eos::PressureFromDensity>);
STATIC_CHECK_FALSE(eos::relationCatalogContains<ValidCatalog, eos::DensityFromSpecificEnthalpy>);
STATIC_CHECK(eos::relationContainsInput<eos::PressureFromDensity, eos::quantity::Density>);
STATIC_CHECK_FALSE(eos::relationContainsInput<eos::PressureFromDensity, eos::quantity::Pressure>);
STATIC_CHECK(std::same_as<eos::RelationOutputT<eos::PressureFromDensity>, eos::quantity::Pressure>);
STATIC_CHECK(std::same_as<eos::RelationInputT<0, eos::PressureFromDensity>, eos::quantity::Density>);
}
TEST_CASE(
"EOS Model Contract Requires Every Declared Relation",
tags::equation_of_state_relation_contract
) {
STATIC_CHECK(eos::EquationOfStateModel<CompleteEquationOfState>);
STATIC_CHECK_FALSE(eos::EquationOfStateModel<MissingRelationImplementation>);
STATIC_CHECK_FALSE(eos::EquationOfStateModel<IncorrectRelationOutput>);
STATIC_CHECK_FALSE(eos::EquationOfStateModel<InvalidRelationCatalog>);
STATIC_CHECK(eos::SupportsRelation<CompleteEquationOfState, eos::PressureFromDensity>);
STATIC_CHECK_FALSE(eos::SupportsRelation<CompleteEquationOfState, eos::DensityFromSpecificEnthalpy>);
STATIC_CHECK_FALSE(CanEvaluateDensityFromSpecificEnthalpy<CompleteEquationOfState>);
STATIC_CHECK(
eos::SupportsPartialDerivative<CompleteEquationOfState, SpecificEnthalpyFromPressureAndEntropy, Entropy>
);
STATIC_CHECK_FALSE(
eos::SupportsPartialDerivative<
CompleteEquationOfState, SpecificEnthalpyFromPressureAndEntropy, ElectronFraction>
);
}
TEST_CASE(
"EOS Evaluation Selects Relations From Typed Inputs",
tags::equation_of_state_relation_contract
) {
constexpr CompleteEquationOfState equationOfState;
constexpr eos::PressureValue pressure =
eos::evaluate<eos::quantity::Pressure>(equationOfState, eos::DensityValue{1.25});
constexpr eos::SpecificEnthalpyValue specificEnthalpy = eos::evaluate<eos::quantity::SpecificEnthalpy>(
equationOfState, eos::PressureValue{0.5}, eos::QuantityValue<Entropy>{0.2}
);
constexpr auto entropyDerivative = eos::partialDerivative<eos::quantity::SpecificEnthalpy, Entropy>(
equationOfState, eos::PressureValue{0.5}, eos::QuantityValue<Entropy>{0.2}
);
STATIC_CHECK(noexcept(eos::evaluate<eos::quantity::Pressure>(equationOfState, eos::DensityValue{1.25})));
STATIC_CHECK(
noexcept(eos::partialDerivative<eos::quantity::SpecificEnthalpy, Entropy>(
equationOfState, eos::PressureValue{0.5}, eos::QuantityValue<Entropy>{0.2}
))
);
STATIC_CHECK(pressure.value() == 2.5);
STATIC_CHECK(specificEnthalpy.value() == 2.5);
STATIC_CHECK(entropyDerivative.value() == 5.0);
}
TEST_CASE(
"EOS Evaluation Errors Retain A Structured Cause",
tags::equation_of_state_relation_contract
) {
const eos::EvaluationError error(
eos::EvaluationErrorCode::outside_domain, "Density is outside the relation domain."
);
CHECK(error.code() == eos::EvaluationErrorCode::outside_domain);
CHECK(std::string_view{error.what()} == "Density is outside the relation domain.");
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,156 @@
#include <array>
#include <cmath>
#include <limits>
#include <stdexcept>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
import mean_field;
import test_helpers;
namespace eos = mean_field::eos;
TEST_CASE(
"Polytropic EOS Pressure To Specific Enthalpy Relation Is Characterized",
tags::polytropic_eos_characterization
) {
constexpr std::array<double, 3> polytropicIndices{1.0, 1.5, 3.0};
constexpr std::array<double, 3> polytropicConstants{0.25, 0.73, 2.0};
constexpr std::array<double, 5> pressures{0.0, 1.0e-12, 1.0e-4, 0.3, 5.0};
for (const double polytropicIndex : polytropicIndices) {
for (const double polytropicConstant : polytropicConstants) {
const mean_field::eos::Polytrope equationOfState(polytropicIndex, polytropicConstant);
for (const double pressure : pressures) {
CAPTURE(polytropicIndex, polytropicConstant, pressure);
const double indexPlusOne = polytropicIndex + 1.0;
const double expectedEnthalpy = indexPlusOne *
std::pow(polytropicConstant, polytropicIndex / indexPlusOne) *
std::pow(pressure, 1.0 / indexPlusOne);
const double enthalpy =
eos::evaluate<eos::quantity::SpecificEnthalpy>(equationOfState, eos::PressureValue{pressure})
.value();
if (pressure == 0.0) {
CHECK(enthalpy == 0.0);
} else {
CHECK_THAT(enthalpy, Catch::Matchers::WithinRel(expectedEnthalpy, 5.0e-14));
const double recoveredPressure =
eos::evaluate<eos::quantity::Pressure>(equationOfState, eos::SpecificEnthalpyValue{enthalpy})
.value();
CHECK_THAT(recoveredPressure, Catch::Matchers::WithinRel(pressure, 5.0e-13));
}
}
}
}
}
TEST_CASE(
"Polytropic EOS Domain Contract Covers Every Relation",
tags::polytropic_eos_characterization
) {
constexpr double infinity = std::numeric_limits<double>::infinity();
constexpr double quietNaN = std::numeric_limits<double>::quiet_NaN();
for (const double invalidIndex : std::array<double, 4>{0.999, infinity, -infinity, quietNaN}) {
CAPTURE(invalidIndex);
CHECK_THROWS_AS(mean_field::eos::Polytrope(invalidIndex, 1.0), std::invalid_argument);
}
for (const double invalidConstant : std::array<double, 5>{0.0, -0.1, infinity, -infinity, quietNaN}) {
CAPTURE(invalidConstant);
CHECK_THROWS_AS(mean_field::eos::Polytrope(3.0, invalidConstant), std::invalid_argument);
}
const mean_field::eos::Polytrope equationOfState(3.0, 0.75);
constexpr double negativeDensity = -0.1;
CHECK_THROWS_AS(
eos::evaluate<eos::quantity::Pressure>(equationOfState, eos::DensityValue{negativeDensity}), std::domain_error
);
CHECK_THROWS_AS(
eos::evaluate<eos::quantity::SpecificEnthalpy>(equationOfState, eos::DensityValue{negativeDensity}),
std::domain_error
);
CHECK_THROWS_AS(
(eos::partialDerivative<eos::quantity::Pressure, eos::quantity::Density>(
equationOfState, eos::DensityValue{negativeDensity}
)),
std::domain_error
);
CHECK_THROWS_AS(
eos::evaluate<eos::quantity::SpecificEnthalpy>(equationOfState, eos::PressureValue{-0.1}), std::domain_error
);
constexpr double exteriorEnthalpy = -0.1;
CHECK(
eos::evaluate<eos::quantity::Density>(equationOfState, eos::SpecificEnthalpyValue{exteriorEnthalpy}).value() ==
0.0
);
CHECK(
eos::evaluate<eos::quantity::Pressure>(equationOfState, eos::SpecificEnthalpyValue{exteriorEnthalpy}).value() ==
0.0
);
CHECK(
(eos::partialDerivative<eos::quantity::Density, eos::quantity::SpecificEnthalpy>(
equationOfState, eos::SpecificEnthalpyValue{exteriorEnthalpy}
)
.value() == 0.0)
);
CHECK(
(eos::partialDerivative<eos::quantity::Pressure, eos::quantity::SpecificEnthalpy>(
equationOfState, eos::SpecificEnthalpyValue{exteriorEnthalpy}
)
.value() == 0.0)
);
for (const double nonfiniteValue : std::array<double, 3>{infinity, -infinity, quietNaN}) {
CAPTURE(nonfiniteValue);
CHECK_THROWS_AS(
eos::evaluate<eos::quantity::Pressure>(equationOfState, eos::DensityValue{nonfiniteValue}),
std::domain_error
);
CHECK_THROWS_AS(
eos::evaluate<eos::quantity::SpecificEnthalpy>(equationOfState, eos::DensityValue{nonfiniteValue}),
std::domain_error
);
CHECK_THROWS_AS(
(eos::partialDerivative<eos::quantity::Pressure, eos::quantity::Density>(
equationOfState, eos::DensityValue{nonfiniteValue}
)),
std::domain_error
);
CHECK_THROWS_AS(
eos::evaluate<eos::quantity::SpecificEnthalpy>(equationOfState, eos::PressureValue{nonfiniteValue}),
std::domain_error
);
CHECK_THROWS_AS(
eos::evaluate<eos::quantity::Density>(equationOfState, eos::SpecificEnthalpyValue{nonfiniteValue}),
std::domain_error
);
CHECK_THROWS_AS(
eos::evaluate<eos::quantity::Pressure>(equationOfState, eos::SpecificEnthalpyValue{nonfiniteValue}),
std::domain_error
);
CHECK_THROWS_AS(
(eos::partialDerivative<eos::quantity::Density, eos::quantity::SpecificEnthalpy>(
equationOfState, eos::SpecificEnthalpyValue{nonfiniteValue}
)),
std::domain_error
);
CHECK_THROWS_AS(
(eos::partialDerivative<eos::quantity::Pressure, eos::quantity::SpecificEnthalpy>(
equationOfState, eos::SpecificEnthalpyValue{nonfiniteValue}
)),
std::domain_error
);
}
}

View File

@@ -0,0 +1,200 @@
#include <array>
#include <cmath>
#include <concepts>
#include <limits>
#include <type_traits>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
import mean_field;
import test_helpers;
namespace {
namespace eos = mean_field::eos;
template <typename Candidate>
concept HasAnyUnaryEquationOfStateConversion =
requires(const Candidate &candidate, const double value) { candidate.pressure_from_density(value); } ||
requires(const Candidate &candidate, const double value) { candidate.pressure_from_enthalpy(value); } ||
requires(const Candidate &candidate, const double value) { candidate.enthalpy_from_density(value); } ||
requires(const Candidate &candidate, const double value) { candidate.enthalpy_from_pressure(value); } ||
requires(const Candidate &candidate, const double value) { candidate.density_from_enthalpy(value); } ||
requires(const Candidate &candidate, const double value) {
candidate.density_derivative_from_enthalpy(value);
} ||
requires(const Candidate &candidate, const double value) {
candidate.pressure_derivative_from_enthalpy(value);
} ||
requires(const Candidate &candidate, const double value) { candidate.pressure_derivative_from_density(value); };
} // namespace
TEST_CASE(
"Polytropic EOS Declares Its Thermodynamic Relation Contract",
tags::polytropic_eos_relation_contract
) {
using Polytrope = eos::Polytrope;
STATIC_CHECK(eos::EquationOfStateModel<Polytrope>);
STATIC_CHECK_FALSE(std::is_polymorphic_v<Polytrope>);
STATIC_CHECK_FALSE(HasAnyUnaryEquationOfStateConversion<Polytrope>);
STATIC_CHECK(Polytrope::Relations::size == 5);
STATIC_CHECK(eos::SupportsRelation<Polytrope, eos::PressureFromDensity>);
STATIC_CHECK(eos::SupportsRelation<Polytrope, eos::PressureFromSpecificEnthalpy>);
STATIC_CHECK(eos::SupportsRelation<Polytrope, eos::SpecificEnthalpyFromDensity>);
STATIC_CHECK(eos::SupportsRelation<Polytrope, eos::SpecificEnthalpyFromPressure>);
STATIC_CHECK(eos::SupportsRelation<Polytrope, eos::DensityFromSpecificEnthalpy>);
STATIC_CHECK(eos::SupportsPartialDerivative<Polytrope, eos::PressureFromDensity, eos::quantity::Density>);
STATIC_CHECK(
eos::SupportsPartialDerivative<Polytrope, eos::PressureFromSpecificEnthalpy, eos::quantity::SpecificEnthalpy>
);
STATIC_CHECK(
eos::SupportsPartialDerivative<Polytrope, eos::DensityFromSpecificEnthalpy, eos::quantity::SpecificEnthalpy>
);
STATIC_CHECK_FALSE(
eos::SupportsPartialDerivative<Polytrope, eos::SpecificEnthalpyFromPressure, eos::quantity::Pressure>
);
STATIC_CHECK_FALSE(
eos::SupportsPartialDerivative<Polytrope, eos::SpecificEnthalpyFromDensity, eos::quantity::Density>
);
}
TEST_CASE(
"Polytropic EOS Typed Relations Preserve Analytic Values",
tags::polytropic_eos_characterization
) {
constexpr std::array<double, 3> polytropicIndices{1.0, 1.5, 3.0};
constexpr std::array<double, 2> polytropicConstants{0.25, 0.73};
constexpr std::array<double, 4> densities{0.0, 1.0e-6, 0.2, 2.0};
constexpr std::array<double, 4> specificEnthalpies{-0.3, 0.0, 0.2, 1.7};
constexpr std::array<double, 4> pressures{0.0, 1.0e-8, 0.3, 4.0};
for (const double polytropicIndex : polytropicIndices) {
for (const double polytropicConstant : polytropicConstants) {
const eos::Polytrope equationOfState(polytropicIndex, polytropicConstant);
for (const double density : densities) {
CAPTURE(polytropicIndex, polytropicConstant, density);
const double expectedPressure = polytropicConstant * std::pow(density, 1.0 + 1.0 / polytropicIndex);
const double expectedSpecificEnthalpy =
(polytropicIndex + 1.0) * polytropicConstant * std::pow(density, 1.0 / polytropicIndex);
CHECK(
eos::evaluate<eos::quantity::Pressure>(equationOfState, eos::DensityValue{density}).value() ==
expectedPressure
);
CHECK(
eos::evaluate<eos::quantity::SpecificEnthalpy>(equationOfState, eos::DensityValue{density})
.value() == expectedSpecificEnthalpy
);
CHECK_THAT(
(eos::partialDerivative<eos::quantity::Pressure, eos::quantity::Density>(
equationOfState, eos::DensityValue{density}
)
.value()),
Catch::Matchers::WithinRel(
density == 0.0 ? 0.0 : expectedSpecificEnthalpy / polytropicIndex, 2.0e-15
)
);
}
for (const double specificEnthalpy : specificEnthalpies) {
CAPTURE(polytropicIndex, polytropicConstant, specificEnthalpy);
const double expectedDensity =
specificEnthalpy <= 0.0
? 0.0
: std::pow(specificEnthalpy / ((polytropicIndex + 1.0) * polytropicConstant), polytropicIndex);
CHECK(
eos::evaluate<eos::quantity::Density>(equationOfState, eos::SpecificEnthalpyValue{specificEnthalpy})
.value() == expectedDensity
);
CHECK(
eos::evaluate<eos::quantity::Pressure>(
equationOfState, eos::SpecificEnthalpyValue{specificEnthalpy}
)
.value() ==
(specificEnthalpy <= 0.0 ? 0.0 : expectedDensity * specificEnthalpy / (polytropicIndex + 1.0))
);
CHECK(
(eos::partialDerivative<eos::quantity::Pressure, eos::quantity::SpecificEnthalpy>(
equationOfState, eos::SpecificEnthalpyValue{specificEnthalpy}
)
.value() == expectedDensity)
);
}
for (const double pressure : pressures) {
CAPTURE(polytropicIndex, polytropicConstant, pressure);
const double indexPlusOne = polytropicIndex + 1.0;
const double expectedSpecificEnthalpy = indexPlusOne *
std::pow(polytropicConstant, polytropicIndex / indexPlusOne) *
std::pow(pressure, 1.0 / indexPlusOne);
CHECK(
eos::evaluate<eos::quantity::SpecificEnthalpy>(equationOfState, eos::PressureValue{pressure})
.value() == expectedSpecificEnthalpy
);
}
}
}
}
TEST_CASE(
"Typed Polytropic EOS Preserves Domain And Exterior Semantics",
tags::polytropic_eos_relation_contract
) {
const eos::Polytrope equationOfState(3.0, 0.75);
try {
static_cast<void>(eos::evaluate<eos::quantity::Pressure>(equationOfState, eos::DensityValue{-0.1}));
FAIL("A negative density must be rejected.");
} catch (const eos::EvaluationError &error) {
CHECK(error.code() == eos::EvaluationErrorCode::outside_domain);
}
try {
static_cast<void>(eos::evaluate<eos::quantity::SpecificEnthalpy>(
equationOfState, eos::PressureValue{std::numeric_limits<double>::quiet_NaN()}
));
FAIL("A nonfinite pressure must be rejected.");
} catch (const eos::EvaluationError &error) {
CHECK(error.code() == eos::EvaluationErrorCode::nonfinite_input);
}
constexpr double exteriorSpecificEnthalpy = -0.3;
CHECK(
eos::evaluate<eos::quantity::Density>(equationOfState, eos::SpecificEnthalpyValue{exteriorSpecificEnthalpy})
.value() == 0.0
);
CHECK(
eos::evaluate<eos::quantity::Pressure>(equationOfState, eos::SpecificEnthalpyValue{exteriorSpecificEnthalpy})
.value() == 0.0
);
CHECK(
(eos::partialDerivative<eos::quantity::Density, eos::quantity::SpecificEnthalpy>(
equationOfState, eos::SpecificEnthalpyValue{exteriorSpecificEnthalpy}
)
.value() == 0.0)
);
CHECK(
(eos::partialDerivative<eos::quantity::Pressure, eos::quantity::SpecificEnthalpy>(
equationOfState, eos::SpecificEnthalpyValue{exteriorSpecificEnthalpy}
)
.value() == 0.0)
);
}

View File

@@ -0,0 +1,329 @@
#include <cmath>
#include <concepts>
#include <limits>
#include <string_view>
#include <type_traits>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
import mean_field;
import test_helpers;
namespace {
namespace eos = mean_field::eos;
namespace field = mean_field::field;
namespace surface = mean_field::surface;
struct Entropy final : eos::ThermodynamicQuantity {
static constexpr std::string_view identifier = "entropy";
};
struct ElectronFraction final : eos::ThermodynamicQuantity {
static constexpr std::string_view identifier = "electron_fraction";
};
struct EntropyField final {
static constexpr std::string_view name = "entropy";
};
struct ElectronFractionField final {
static constexpr std::string_view name = "electron_fraction";
};
using SpecificEnthalpyFromPressureEntropyAndElectronFraction =
eos::Relation<eos::quantity::SpecificEnthalpy, eos::quantity::Pressure, Entropy, ElectronFraction>;
class GeneralStellarMatterEquationOfState final {
public:
using Relations = eos::RelationCatalog<SpecificEnthalpyFromPressureEntropyAndElectronFraction>;
[[nodiscard]] constexpr eos::SpecificEnthalpyValue evaluate(
SpecificEnthalpyFromPressureEntropyAndElectronFraction,
const eos::PressureValue pressure,
const eos::QuantityValue<Entropy> entropy,
const eos::QuantityValue<ElectronFraction> electronFraction
) const noexcept {
return eos::SpecificEnthalpyValue{
2.0 * pressure.value() + 3.0 * entropy.value() + 5.0 * electronFraction.value()
};
}
[[nodiscard]] constexpr eos::PartialDerivative<
eos::quantity::SpecificEnthalpy,
Entropy>
partialDerivative(
SpecificEnthalpyFromPressureEntropyAndElectronFraction,
eos::WithRespectTo<Entropy>,
eos::PressureValue,
eos::QuantityValue<Entropy>,
eos::QuantityValue<ElectronFraction>
) const noexcept {
return eos::PartialDerivative<eos::quantity::SpecificEnthalpy, Entropy>{3.0};
}
[[nodiscard]] constexpr eos::PartialDerivative<
eos::quantity::SpecificEnthalpy,
ElectronFraction>
partialDerivative(
SpecificEnthalpyFromPressureEntropyAndElectronFraction,
eos::WithRespectTo<ElectronFraction>,
eos::PressureValue,
eos::QuantityValue<Entropy>,
eos::QuantityValue<ElectronFraction>
) const noexcept {
return eos::PartialDerivative<eos::quantity::SpecificEnthalpy, ElectronFraction>{5.0};
}
};
class GeneralEquationOfStateWithoutElectronFractionPartial final {
public:
using Relations = eos::RelationCatalog<SpecificEnthalpyFromPressureEntropyAndElectronFraction>;
[[nodiscard]] constexpr eos::SpecificEnthalpyValue evaluate(
SpecificEnthalpyFromPressureEntropyAndElectronFraction,
const eos::PressureValue pressure,
const eos::QuantityValue<Entropy> entropy,
const eos::QuantityValue<ElectronFraction> electronFraction
) const noexcept {
return eos::SpecificEnthalpyValue{pressure.value() + entropy.value() + electronFraction.value()};
}
[[nodiscard]] constexpr eos::PartialDerivative<
eos::quantity::SpecificEnthalpy,
Entropy>
partialDerivative(
SpecificEnthalpyFromPressureEntropyAndElectronFraction,
eos::WithRespectTo<Entropy>,
eos::PressureValue,
eos::QuantityValue<Entropy>,
eos::QuantityValue<ElectronFraction>
) const noexcept {
return eos::PartialDerivative<eos::quantity::SpecificEnthalpy, Entropy>{1.0};
}
};
using SpecificEnthalpyFromPressureAndEntropy =
eos::Relation<eos::quantity::SpecificEnthalpy, eos::quantity::Pressure, Entropy>;
class AmbiguousSurfaceEquationOfState final {
public:
using Relations =
eos::RelationCatalog<eos::SpecificEnthalpyFromPressure, SpecificEnthalpyFromPressureAndEntropy>;
[[nodiscard]] constexpr eos::SpecificEnthalpyValue evaluate(
eos::SpecificEnthalpyFromPressure,
const eos::PressureValue pressure
) const noexcept {
return eos::SpecificEnthalpyValue{pressure.value()};
}
[[nodiscard]] constexpr eos::SpecificEnthalpyValue evaluate(
SpecificEnthalpyFromPressureAndEntropy,
const eos::PressureValue pressure,
const eos::QuantityValue<Entropy> entropy
) const noexcept {
return eos::SpecificEnthalpyValue{pressure.value() + entropy.value()};
}
[[nodiscard]] constexpr eos::PartialDerivative<
eos::quantity::SpecificEnthalpy,
Entropy>
partialDerivative(
SpecificEnthalpyFromPressureAndEntropy,
eos::WithRespectTo<Entropy>,
eos::PressureValue,
eos::QuantityValue<Entropy>
) const noexcept {
return eos::PartialDerivative<eos::quantity::SpecificEnthalpy, Entropy>{1.0};
}
};
class DensityOnlyEquationOfState final {
public:
using Relations = eos::RelationCatalog<eos::PressureFromDensity>;
[[nodiscard]] constexpr eos::PressureValue evaluate(
eos::PressureFromDensity,
const eos::DensityValue density
) const noexcept {
return eos::PressureValue{density.value()};
}
};
using GeneralSurfaceFormulation = surface::SurfaceConstraintFormulation<
eos::quantity::SpecificEnthalpy,
field::Enthalpy,
surface::SurfaceStateBindings<
surface::SurfaceStateBinding<eos::quantity::SpecificEnthalpy, field::Enthalpy>,
surface::SurfaceStateBinding<Entropy, EntropyField>,
surface::SurfaceStateBinding<ElectronFraction, ElectronFractionField>>>;
struct PolytropicSurfaceState final {
double specificEnthalpy;
[[nodiscard]] eos::SpecificEnthalpyValue value(eos::quantity::SpecificEnthalpy) const noexcept {
return eos::SpecificEnthalpyValue{specificEnthalpy};
}
};
struct GeneralSurfaceState final {
double specificEnthalpy;
double entropy;
double electronFraction;
[[nodiscard]] eos::SpecificEnthalpyValue value(eos::quantity::SpecificEnthalpy) const noexcept {
return eos::SpecificEnthalpyValue{specificEnthalpy};
}
[[nodiscard]] eos::QuantityValue<Entropy> value(Entropy) const noexcept {
return eos::QuantityValue<Entropy>{entropy};
}
[[nodiscard]] eos::QuantityValue<ElectronFraction> value(ElectronFraction) const noexcept {
return eos::QuantityValue<ElectronFraction>{electronFraction};
}
};
template <typename Candidate>
concept HasTargetEnthalpy = requires(const Candidate &candidate) { candidate.targetEnthalpy; };
} // namespace
TEST_CASE(
"Constant Pressure Surface Prescribes Only A Pressure Quantity",
tags::surface_prescription_type_contract
) {
STATIC_CHECK(std::same_as<surface::ConstantPressureSurface::PhysicalQuantity, eos::quantity::Pressure>);
STATIC_CHECK(std::constructible_from<surface::ConstantPressureSurface, eos::PressureValue>);
STATIC_CHECK_FALSE(std::constructible_from<surface::ConstantPressureSurface, eos::SpecificEnthalpyValue>);
STATIC_CHECK_FALSE(std::constructible_from<surface::ConstantPressureSurface, double>);
STATIC_CHECK(std::same_as<surface::Isobaric, surface::ConstantPressureSurface>);
STATIC_CHECK(std::is_trivially_copyable_v<surface::ConstantPressureSurface>);
STATIC_CHECK(std::is_trivially_copyable_v<surface::PressureSurfaceDescriptor>);
STATIC_CHECK(std::is_trivially_copyable_v<surface::RuntimeSurfaceConstraintDependencies>);
const surface::ConstantPressureSurface pressureSurface{eos::PressureValue{0.03125}};
CHECK(pressureSurface.targetPressure() == eos::PressureValue{0.03125});
CHECK(pressureSurface.descriptor().targetPressure == 0.03125);
CHECK_THROWS_AS(surface::ConstantPressureSurface{eos::PressureValue{-0.1}}, std::invalid_argument);
CHECK_THROWS_AS(
surface::ConstantPressureSurface{eos::PressureValue{std::numeric_limits<double>::infinity()}},
std::invalid_argument
);
}
TEST_CASE(
"Polytropic EOS Resolves Constant Surface Pressure Through Its Enthalpy Relation",
tags::surface_constraint_compilation
) {
using Formulation = surface::BarotropicSurfaceFormulation;
STATIC_CHECK(surface::PressureSurfaceCompilable<Formulation, eos::Polytrope>);
STATIC_CHECK_FALSE(surface::PressureSurfaceCompilable<Formulation, DensityOnlyEquationOfState>);
const eos::Polytrope equationOfState(3.0, 0.25);
const surface::ConstantPressureSurface pressureSurface{eos::PressureValue{0.03125}};
const auto constraint = surface::compilePressureSurfaceConstraint<Formulation>(pressureSurface, equationOfState);
using Constraint = std::remove_cvref_t<decltype(constraint)>;
using Dependencies = Constraint::SurfaceDependencies;
STATIC_CHECK(std::is_trivially_copyable_v<Constraint>);
STATIC_CHECK(std::same_as<Constraint::Relation, eos::SpecificEnthalpyFromPressure>);
STATIC_CHECK(std::same_as<Dependencies::RowField, field::Enthalpy>);
STATIC_CHECK(std::same_as<Dependencies::StateFieldTypes, field::TypeList<field::Enthalpy>>);
STATIC_CHECK_FALSE(HasTargetEnthalpy<Constraint>);
const double requiredSpecificEnthalpy =
eos::evaluate<eos::quantity::SpecificEnthalpy>(equationOfState, pressureSurface.targetPressure()).value();
const PolytropicSurfaceState state{requiredSpecificEnthalpy};
const PolytropicSurfaceState variation{-0.19};
CHECK(constraint.targetPressure() == eos::PressureValue{0.03125});
CHECK(constraint.residual(state) == 0.0);
CHECK(constraint.jacobianAction(state, variation) == -0.19);
const auto runtimeDependencies = constraint.runtimeDependencies();
REQUIRE(runtimeDependencies.stateFields.size() == 1);
CHECK(runtimeDependencies.residualRowField == surface::surfaceFieldId<field::Enthalpy>);
CHECK(runtimeDependencies.stateFields[0] == surface::surfaceFieldId<field::Enthalpy>);
}
TEST_CASE(
"General EOS Resolves Constant Surface Pressure With Local Composition",
tags::surface_constraint_compilation
) {
STATIC_CHECK(surface::PressureSurfaceCompilable<GeneralSurfaceFormulation, GeneralStellarMatterEquationOfState>);
STATIC_CHECK_FALSE(
surface::PressureSurfaceCompilable<surface::BarotropicSurfaceFormulation, GeneralStellarMatterEquationOfState>
);
STATIC_CHECK_FALSE(
surface::PressureSurfaceCompilable<
GeneralSurfaceFormulation, GeneralEquationOfStateWithoutElectronFractionPartial>
);
STATIC_CHECK_FALSE(surface::PressureSurfaceCompilable<GeneralSurfaceFormulation, AmbiguousSurfaceEquationOfState>);
const GeneralStellarMatterEquationOfState equationOfState;
const surface::ConstantPressureSurface pressureSurface{eos::PressureValue{0.4}};
const auto constraint =
surface::compilePressureSurfaceConstraint<GeneralSurfaceFormulation>(pressureSurface, equationOfState);
using Constraint = std::remove_cvref_t<decltype(constraint)>;
using Dependencies = Constraint::SurfaceDependencies;
STATIC_CHECK(std::same_as<Constraint::Relation, SpecificEnthalpyFromPressureEntropyAndElectronFraction>);
STATIC_CHECK(
std::same_as<
Dependencies::StateFieldTypes, field::TypeList<field::Enthalpy, EntropyField, ElectronFractionField>>
);
constexpr GeneralSurfaceState firstSurface{
.specificEnthalpy = 2.0 * 0.4 + 3.0 * 0.2 + 5.0 * 0.1, .entropy = 0.2, .electronFraction = 0.1
};
constexpr GeneralSurfaceState secondSurface{
.specificEnthalpy = 2.0 * 0.4 + 3.0 * 0.3 + 5.0 * 0.1, .entropy = 0.3, .electronFraction = 0.1
};
CHECK(firstSurface.specificEnthalpy != secondSurface.specificEnthalpy);
CHECK(constraint.residual(firstSurface) == 0.0);
CHECK(constraint.residual(secondSurface) == 0.0);
const auto runtimeDependencies = constraint.runtimeDependencies();
REQUIRE(runtimeDependencies.stateFields.size() == 3);
CHECK(runtimeDependencies.stateFields[0] == surface::surfaceFieldId<field::Enthalpy>);
CHECK(runtimeDependencies.stateFields[1] == surface::surfaceFieldId<EntropyField>);
CHECK(runtimeDependencies.stateFields[2] == surface::surfaceFieldId<ElectronFractionField>);
}
TEST_CASE(
"General EOS Pressure Surface Jacobian Includes Every Local State Dependency",
tags::surface_constraint_jacobian
) {
const GeneralStellarMatterEquationOfState equationOfState;
const surface::ConstantPressureSurface pressureSurface{eos::PressureValue{0.4}};
const auto constraint =
surface::compilePressureSurfaceConstraint<GeneralSurfaceFormulation>(pressureSurface, equationOfState);
constexpr GeneralSurfaceState state{.specificEnthalpy = 1.7, .entropy = 0.2, .electronFraction = 0.1};
constexpr GeneralSurfaceState variation{.specificEnthalpy = 0.7, .entropy = -0.2, .electronFraction = 0.05};
constexpr double step = 1.0e-7;
const GeneralSurfaceState forward{
.specificEnthalpy = state.specificEnthalpy + step * variation.specificEnthalpy,
.entropy = state.entropy + step * variation.entropy,
.electronFraction = state.electronFraction + step * variation.electronFraction
};
const GeneralSurfaceState backward{
.specificEnthalpy = state.specificEnthalpy - step * variation.specificEnthalpy,
.entropy = state.entropy - step * variation.entropy,
.electronFraction = state.electronFraction - step * variation.electronFraction
};
const double finiteDifference = (constraint.residual(forward) - constraint.residual(backward)) / (2.0 * step);
const double jacobianAction = constraint.jacobianAction(state, variation);
CHECK(jacobianAction == variation.specificEnthalpy - 3.0 * variation.entropy - 5.0 * variation.electronFraction);
CHECK_THAT(finiteDifference, Catch::Matchers::WithinAbs(jacobianAction, 2.0e-9));
}

View File

@@ -1,76 +0,0 @@
#include <cmath>
#include <limits>
#include <stdexcept>
#include <catch2/catch_test_macros.hpp>
import mean_field;
import test_helpers;
TEST_CASE(
"Isobaric Surface Resolves Zero Pressure To Zero Enthalpy",
tags::barotrope &tags::unit &tags::surface
) {
const mean_field::eos::Polytrope equationOfState(3.0, 0.25);
const mean_field::surface::Isobaric surface;
const mean_field::surface::ResolvedSurfaceCondition resolved = surface.resolve(equationOfState);
CHECK(surface.targetPressure() == 0.0);
CHECK(resolved.targetEnthalpy == 0.0);
CHECK(resolved.residual(0.0) == 0.0);
CHECK(resolved.residual(0.37) == 0.37);
CHECK(resolved.jacobianAction(-0.19) == -0.19);
}
TEST_CASE(
"Isobaric Surface Resolves Positive Pressure Through The EOS",
tags::barotrope &tags::unit &tags::surface
) {
const mean_field::eos::Polytrope equationOfState(3.0, 0.25);
constexpr double targetPressure = 0.03125;
const mean_field::surface::Isobaric surface(targetPressure);
const mean_field::surface::ResolvedSurfaceCondition resolved = surface.resolve(equationOfState);
const double recoveredPressure = equationOfState.pressure_from_enthalpy(resolved.targetEnthalpy);
INFO("Resolved surface enthalpy = " << resolved.targetEnthalpy);
INFO("Recovered surface pressure = " << recoveredPressure);
CHECK(resolved.targetEnthalpy > 0.0);
CHECK(std::abs(recoveredPressure - targetPressure) < 64.0 * std::numeric_limits<double>::epsilon());
CHECK(resolved.residual(resolved.targetEnthalpy) == 0.0);
}
TEST_CASE(
"Isobaric Surface Rejects Invalid Pressure Targets",
tags::barotrope &tags::unit &tags::surface
) {
CHECK_THROWS_AS(mean_field::surface::Isobaric(-1.0), std::invalid_argument);
CHECK_THROWS_AS(mean_field::surface::Isobaric(std::numeric_limits<double>::infinity()), std::invalid_argument);
CHECK_THROWS_AS(mean_field::surface::Isobaric(std::numeric_limits<double>::quiet_NaN()), std::invalid_argument);
}
TEST_CASE(
"Surface Base Dispatch Preserves The Isobaric Prescription",
tags::barotrope &tags::unit &tags::surface
) {
const mean_field::eos::Polytrope equationOfState(3.0, 0.25);
const mean_field::surface::Isobaric isobaric(0.02);
const mean_field::surface::SurfaceBase &surface = isobaric;
surface.validate(equationOfState);
const mean_field::surface::ResolvedSurfaceCondition resolved = surface.resolve(equationOfState);
CHECK(resolved.targetEnthalpy > 0.0);
CHECK(resolved.residual(resolved.targetEnthalpy) == 0.0);
}

View File

@@ -13,456 +13,469 @@ export module test_helpers;
import mean_field;
template <std::size_t N> struct Tag {
std::array<char, N> chars{};
std::array<char, N> chars{};
// ReSharper disable once CppNonExplicitConvertingConstructor
consteval Tag(std::array<char, N> arr) : chars(arr) {}
// ReSharper disable once CppNonExplicitConvertingConstructor
consteval Tag(
std::array<
char,
N> arr
)
: chars(arr) {
}
// ReSharper disable once CppNonExplicitConversionOperator
constexpr operator const char *() const { return chars.data(); }
// ReSharper disable once CppNonExplicitConversionOperator
constexpr operator const char *() const {
return chars.data();
}
// ReSharper disable once CppNonExplicitConversionOperator
constexpr operator Catch::StringRef() const {
return Catch::StringRef(chars.data(), N - 1);
}
// ReSharper disable once CppNonExplicitConversionOperator
constexpr operator Catch::StringRef() const {
return Catch::StringRef(chars.data(), N - 1);
}
template <std::size_t M>
consteval Tag<N + M - 1> operator&(const Tag<M> &other) const {
std::array<char, N + M - 1> res{};
std::ranges::copy(chars.begin(), chars.end() - 1, res.begin());
std::ranges::copy(other.chars, res.begin() + (N - 1));
return {res};
}
template <std::size_t M> consteval Tag<N + M - 1> operator&(const Tag<M> &other) const {
std::array<char, N + M - 1> res{};
std::ranges::copy(chars.begin(), chars.end() - 1, res.begin());
std::ranges::copy(other.chars, res.begin() + (N - 1));
return {res};
}
};
template <std::size_t N> consteval auto make_tag(const char (&str)[N]) {
std::array<char, N + 2> res{};
res[0] = '[';
std::ranges::copy(str, str + N - 1, res.begin() + 1);
res[N] = ']';
res[N + 1] = '\0';
return Tag<N + 2>{res};
std::array<char, N + 2> res{};
res[0] = '[';
std::ranges::copy(str, str + N - 1, res.begin() + 1);
res[N] = ']';
res[N + 1] = '\0';
return Tag<N + 2>{res};
}
template <std::size_t N, std::size_t M>
consteval auto sub_tag(const Tag<N> &parent, const char (&str)[M]) {
return parent & make_tag(str);
template <
std::size_t N,
std::size_t M>
consteval auto sub_tag(
const Tag<N> &parent,
const char (&str)[M]
) {
return parent & make_tag(str);
}
namespace test_utils::detail {
std::optional<mean_field::utils::Args> configured_args;
std::optional<mean_field::utils::Args> configured_args;
mean_field::utils::Args make_default_args() {
mean_field::utils::Args args;
args.mesh_file = "sandbox.smesh";
args.p.rtol = 1.0e-12;
args.p.atol = 1.0e-12;
return args;
}
mean_field::utils::Args make_default_args() {
mean_field::utils::Args args;
args.mesh_file = "sandbox.smesh";
args.p.rtol = 1.0e-12;
args.p.atol = 1.0e-12;
return args;
}
} // namespace test_utils::detail
export namespace test_utils {
void set_args(mean_field::utils::Args args) {
detail::configured_args = std::move(args);
}
void set_args(mean_field::utils::Args args) {
detail::configured_args = std::move(args);
}
mean_field::utils::Args setup_args() {
if (detail::configured_args.has_value()) {
return *detail::configured_args;
}
mean_field::utils::Args setup_args() {
if (detail::configured_args.has_value()) {
return *detail::configured_args;
}
return detail::make_default_args();
}
return detail::make_default_args();
}
} // namespace test_utils
export namespace gravity_prepared_test_utils {
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
template <typename FieldT>
inline mean_field::field::FieldDofMap
make_field_map(const mean_field::fem::FEM &f) {
if constexpr (std::same_as<FieldT, mean_field::field::Density>) {
return mean_field::field::make_field_dof_map<FieldT, DomainSchema>(
*f.densityFes);
} else if constexpr (std::same_as<FieldT, mean_field::field::Displacement>) {
return mean_field::field::make_field_dof_map<FieldT, DomainSchema>(
*f.displacementFes);
} else {
static_assert(std::same_as<FieldT, mean_field::field::Gravity>);
return mean_field::field::make_field_dof_map<FieldT, DomainSchema>(
*f.gravityFluxFes);
}
}
template <typename FieldT>
inline mfem::Vector gather_field(const mean_field::fem::FEM &f,
const mfem::Vector &true_vector) {
return make_field_map<FieldT>(f).gather(true_vector);
}
inline mfem::Vector make_deterministic_vector(const int size,
const double phase = 0.0) {
mfem::Vector vector(size);
for (int i = 0; i < size; ++i) {
const double index = static_cast<double>(i + 1);
vector(i) = std::sin(0.37 * index + phase) +
0.31 * std::cos(0.19 * index - 0.5 * phase);
}
return vector;
}
inline mfem::Vector make_displacement(const mean_field::fem::FEM &f,
const double scale) {
mfem::ParGridFunction displacement(f.displacementFes.get());
auto displacement_function = [scale](const mfem::Vector &position,
mfem::Vector &value) {
value.SetSize(3);
value(0) = scale * (0.04 * position(0) + 0.01 * position(1) * position(2));
value(1) =
scale * (-0.03 * position(1) + 0.008 * position(0) * position(2));
value(2) = scale * (0.02 * position(2) - 0.006 * position(0) * position(1));
};
mfem::VectorFunctionCoefficient coefficient(f.mesh->Dimension(),
displacement_function);
displacement.ProjectCoefficient(coefficient);
mfem::Vector displacement_true;
displacement.GetTrueDofs(displacement_true);
return displacement_true;
}
inline mfem::Vector make_domain_supported_density(const mean_field::fem::FEM &f,
const bool stellar) {
mfem::Vector attribute_values(f.mesh->attributes.Max());
attribute_values = 0.0;
using DomainSchema =
mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
for (int i = 0; i < f.mesh->attributes.Size(); ++i) {
const int attribute = f.mesh->attributes[i];
const bool is_stellar = DomainSchema::template attribute_belongs_to<
mean_field::utils::domain::Stellar>(attribute);
if (is_stellar == stellar) {
attribute_values(attribute - 1) = 1.0;
template <typename FieldT> inline mean_field::field::FieldDofMap make_field_map(const mean_field::fem::FEM &f) {
if constexpr (std::same_as<FieldT, mean_field::field::Density>) {
return mean_field::field::make_field_dof_map<FieldT, DomainSchema>(*f.densityFes);
} else if constexpr (std::same_as<FieldT, mean_field::field::Displacement>) {
return mean_field::field::make_field_dof_map<FieldT, DomainSchema>(*f.displacementFes);
} else {
static_assert(std::same_as<FieldT, mean_field::field::Gravity>);
return mean_field::field::make_field_dof_map<FieldT, DomainSchema>(*f.gravityFluxFes);
}
}
}
mfem::PWConstCoefficient coefficient(attribute_values);
mfem::ParGridFunction density(f.densityFes.get());
density.ProjectCoefficient(coefficient);
template <typename FieldT>
inline mfem::Vector gather_field(
const mean_field::fem::FEM &f,
const mfem::Vector &true_vector
) {
return make_field_map<FieldT>(f).gather(true_vector);
}
mfem::Vector density_true;
density.GetTrueDofs(density_true);
return density_true;
}
inline mfem::Vector make_deterministic_vector(
const int size,
const double phase = 0.0
) {
mfem::Vector vector(size);
inline mfem::Vector linear_combination(const mfem::Vector &first,
const double first_scale,
const mfem::Vector &second,
const double second_scale) {
MFEM_VERIFY(first.Size() == second.Size(),
"Cannot combine vectors with different sizes.");
for (int i = 0; i < size; ++i) {
const double index = static_cast<double>(i + 1);
vector(i) = std::sin(0.37 * index + phase) + 0.31 * std::cos(0.19 * index - 0.5 * phase);
}
mfem::Vector combination(first);
combination *= first_scale;
combination.Add(second_scale, second);
return combination;
}
return vector;
}
inline double global_norm(const mfem::Vector &vector, MPI_Comm communicator) {
const double local_norm_squared = vector * vector;
double global_norm_squared = 0.0;
MPI_Allreduce(&local_norm_squared, &global_norm_squared, 1, MPI_DOUBLE,
MPI_SUM, communicator);
return std::sqrt(global_norm_squared);
}
inline mfem::Vector make_displacement(
const mean_field::fem::FEM &f,
const double scale
) {
mfem::ParGridFunction displacement(f.displacementFes.get());
inline double global_dot(const mfem::Vector &first, const mfem::Vector &second,
MPI_Comm communicator) {
MFEM_VERIFY(first.Size() == second.Size(),
"Cannot take the dot product of vectors with different sizes.");
auto displacement_function = [scale](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(3);
value(0) = scale * (0.04 * position(0) + 0.01 * position(1) * position(2));
value(1) = scale * (-0.03 * position(1) + 0.008 * position(0) * position(2));
value(2) = scale * (0.02 * position(2) - 0.006 * position(0) * position(1));
};
const double local_dot = first * second;
double global_dot = 0.0;
MPI_Allreduce(&local_dot, &global_dot, 1, MPI_DOUBLE, MPI_SUM, communicator);
return global_dot;
}
mfem::VectorFunctionCoefficient coefficient(f.mesh->Dimension(), displacement_function);
displacement.ProjectCoefficient(coefficient);
inline double relative_error(const mfem::Vector &computed,
const mfem::Vector &reference,
MPI_Comm communicator) {
MFEM_VERIFY(computed.Size() == reference.Size(),
"Cannot compare vectors with different sizes.");
mfem::Vector displacement_true;
displacement.GetTrueDofs(displacement_true);
return displacement_true;
}
mfem::Vector difference(computed);
difference -= reference;
inline mfem::Vector make_domain_supported_density(
const mean_field::fem::FEM &f,
const bool stellar
) {
mfem::Vector attribute_values(f.mesh->attributes.Max());
attribute_values = 0.0;
return global_norm(difference, communicator) /
std::max(global_norm(reference, communicator),
std::numeric_limits<double>::epsilon());
}
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
inline double relative_scalar_error(const double computed,
const double reference) {
return std::abs(computed - reference) /
std::max(std::abs(reference), std::numeric_limits<double>::epsilon());
}
for (int i = 0; i < f.mesh->attributes.Size(); ++i) {
const int attribute = f.mesh->attributes[i];
const bool is_stellar =
DomainSchema::template attribute_belongs_to<mean_field::utils::domain::Stellar>(attribute);
if (is_stellar == stellar) {
attribute_values(attribute - 1) = 1.0;
}
}
mfem::PWConstCoefficient coefficient(attribute_values);
mfem::ParGridFunction density(f.densityFes.get());
density.ProjectCoefficient(coefficient);
mfem::Vector density_true;
density.GetTrueDofs(density_true);
return density_true;
}
inline mfem::Vector linear_combination(
const mfem::Vector &first,
const double first_scale,
const mfem::Vector &second,
const double second_scale
) {
MFEM_VERIFY(first.Size() == second.Size(), "Cannot combine vectors with different sizes.");
mfem::Vector combination(first);
combination *= first_scale;
combination.Add(second_scale, second);
return combination;
}
inline double global_norm(
const mfem::Vector &vector,
MPI_Comm communicator
) {
const double local_norm_squared = vector * vector;
double global_norm_squared = 0.0;
MPI_Allreduce(&local_norm_squared, &global_norm_squared, 1, MPI_DOUBLE, MPI_SUM, communicator);
return std::sqrt(global_norm_squared);
}
inline double global_dot(
const mfem::Vector &first,
const mfem::Vector &second,
MPI_Comm communicator
) {
MFEM_VERIFY(first.Size() == second.Size(), "Cannot take the dot product of vectors with different sizes.");
const double local_dot = first * second;
double global_dot = 0.0;
MPI_Allreduce(&local_dot, &global_dot, 1, MPI_DOUBLE, MPI_SUM, communicator);
return global_dot;
}
inline double relative_error(
const mfem::Vector &computed,
const mfem::Vector &reference,
MPI_Comm communicator
) {
MFEM_VERIFY(computed.Size() == reference.Size(), "Cannot compare vectors with different sizes.");
mfem::Vector difference(computed);
difference -= reference;
return global_norm(difference, communicator) /
std::max(global_norm(reference, communicator), std::numeric_limits<double>::epsilon());
}
inline double relative_scalar_error(
const double computed,
const double reference
) {
return std::abs(computed - reference) / std::max(std::abs(reference), std::numeric_limits<double>::epsilon());
}
} // namespace gravity_prepared_test_utils
export namespace field_dof_test_utils {
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
inline mean_field::mapping::DomainMapper make_domain_mapper() {
const mean_field::utils::Args args = test_utils::setup_args();
return mean_field::mapping::DomainMapper(
args.domain_mapper_options,
std::make_unique<const mean_field::mapping::compactification::
KelvinCompactification>(args.kelvin_options));
}
inline mean_field::mapping::DomainMapper make_domain_mapper() {
const mean_field::utils::Args args = test_utils::setup_args();
return mean_field::mapping::DomainMapper(
args.domain_mapper_options,
std::make_unique<const mean_field::mapping::compactification::KelvinCompactification>(args.kelvin_options)
);
}
inline constexpr int vacuum_material_attribute =
DomainSchema::template material_attribute<
mean_field::utils::domain::Vacuum>();
inline constexpr int vacuum_material_attribute =
DomainSchema::template material_attribute<mean_field::utils::domain::Vacuum>();
template <typename FieldT>
inline mean_field::field::FieldDofMap
make_map(const mfem::ParFiniteElementSpace &finiteElementSpace) {
return mean_field::field::make_field_dof_map<FieldT, DomainSchema>(
finiteElementSpace);
}
template <typename FieldT>
inline mean_field::field::FieldDofMap make_map(const mfem::ParFiniteElementSpace &finiteElementSpace) {
return mean_field::field::make_field_dof_map<FieldT, DomainSchema>(finiteElementSpace);
}
template <typename FieldT>
inline mfem::Vector make_deterministic_supported_vector(
const mfem::ParFiniteElementSpace &finiteElementSpace, const double phase) {
const mean_field::field::FieldDofMap map =
make_map<FieldT>(finiteElementSpace);
const mfem::Vector full =
gravity_prepared_test_utils::make_deterministic_vector(map.full_size(),
phase);
return map.gather(full);
}
template <typename FieldT>
inline mfem::Vector make_deterministic_supported_vector(
const mfem::ParFiniteElementSpace &finiteElementSpace,
const double phase
) {
const mean_field::field::FieldDofMap map = make_map<FieldT>(finiteElementSpace);
const mfem::Vector full = gravity_prepared_test_utils::make_deterministic_vector(map.full_size(), phase);
return map.gather(full);
}
inline mfem::Vector make_supported_displacement(const mean_field::fem::FEM &f,
const double phase) {
const mean_field::field::FieldDofMap map =
make_map<mean_field::field::Displacement>(*f.displacementFes);
return map.gather(gravity_prepared_test_utils::make_displacement(f, phase));
}
inline mfem::Vector make_supported_displacement(
const mean_field::fem::FEM &f,
const double phase
) {
const mean_field::field::FieldDofMap map = make_map<mean_field::field::Displacement>(*f.displacementFes);
return map.gather(gravity_prepared_test_utils::make_displacement(f, phase));
}
inline void apply_hydrostatic_reference(
const mean_field::fem::FEM &f,
const mean_field::physics::RigidRotation &rotation,
const mfem::Vector &enthalpy, const mfem::Vector &gravityPotential,
const mfem::Vector &displacement, const double bernoulliConstant,
mfem::Vector &residual) {
const mean_field::field::FieldDofMap enthalpyMap =
make_map<mean_field::field::Enthalpy>(*f.enthalpyFes);
const mean_field::field::FieldDofMap gravityPotentialMap =
make_map<mean_field::field::Gravity>(*f.gravityPotentialFes);
const mean_field::field::FieldDofMap displacementMap =
make_map<mean_field::field::Displacement>(*f.displacementFes);
inline void apply_hydrostatic_reference(
const mean_field::fem::FEM &f,
const mean_field::physics::RigidRotation &rotation,
const mfem::Vector &enthalpy,
const mfem::Vector &gravityPotential,
const mfem::Vector &displacement,
const double bernoulliConstant,
mfem::Vector &residual
) {
const mean_field::field::FieldDofMap enthalpyMap = make_map<mean_field::field::Enthalpy>(*f.enthalpyFes);
const mean_field::field::FieldDofMap gravityPotentialMap =
make_map<mean_field::field::Gravity>(*f.gravityPotentialFes);
const mean_field::field::FieldDofMap displacementMap =
make_map<mean_field::field::Displacement>(*f.displacementFes);
mfem::Vector enthalpyTrue(enthalpyMap.full_size());
mfem::Vector gravityPotentialTrue(gravityPotentialMap.full_size());
mfem::Vector displacementTrue(displacementMap.full_size());
mfem::Vector residualTrue;
mfem::Vector enthalpyTrue(enthalpyMap.full_size());
mfem::Vector gravityPotentialTrue(gravityPotentialMap.full_size());
mfem::Vector displacementTrue(displacementMap.full_size());
mfem::Vector residualTrue;
enthalpyMap.scatter(enthalpy, enthalpyTrue);
gravityPotentialMap.scatter(gravityPotential, gravityPotentialTrue);
displacementMap.scatter(displacement, displacementTrue);
enthalpyMap.scatter(enthalpy, enthalpyTrue);
gravityPotentialMap.scatter(gravityPotential, gravityPotentialTrue);
displacementMap.scatter(displacement, displacementTrue);
mean_field::operators::kernels::apply_hydrostatic_equilibrium(
f, *f.domainMapperStateless, rotation, enthalpyTrue, gravityPotentialTrue,
displacementTrue, bernoulliConstant, residualTrue);
mean_field::operators::kernels::apply_hydrostatic_equilibrium(
f, *f.domainMapperStateless, rotation, enthalpyTrue, gravityPotentialTrue, displacementTrue,
bernoulliConstant, residualTrue
);
residual.SetSize(enthalpyMap.reduced_size());
enthalpyMap.gather(residualTrue, residual);
}
residual.SetSize(enthalpyMap.reduced_size());
enthalpyMap.gather(residualTrue, residual);
}
} // namespace field_dof_test_utils
export namespace tags {
inline constexpr auto geometry = make_tag("geometry");
inline constexpr auto physics = make_tag("physics");
inline constexpr auto unit = make_tag("unit");
inline constexpr auto mesh = make_tag("mesh");
inline constexpr auto integration = make_tag("integration");
inline constexpr auto solver = make_tag("solver");
inline constexpr auto integrator = make_tag("integrator");
inline constexpr auto mapping = make_tag("mapping");
inline constexpr auto utils = make_tag("utils");
inline constexpr auto mfem_operators = make_tag("operators");
inline constexpr auto initialization = make_tag("initialization");
inline constexpr auto accuracy = make_tag("accuracy");
inline constexpr auto closure = make_tag("closure");
inline constexpr auto kernels = make_tag("kernels");
inline constexpr auto surface = make_tag("surface");
inline constexpr auto model = make_tag("model");
inline constexpr auto geometry = make_tag("geometry");
inline constexpr auto physics = make_tag("physics");
inline constexpr auto unit = make_tag("unit");
inline constexpr auto mesh = make_tag("mesh");
inline constexpr auto integration = make_tag("integration");
inline constexpr auto solver = make_tag("solver");
inline constexpr auto integrator = make_tag("integrator");
inline constexpr auto mapping = make_tag("mapping");
inline constexpr auto utils = make_tag("utils");
inline constexpr auto mfem_operators = make_tag("operators");
inline constexpr auto initialization = make_tag("initialization");
inline constexpr auto accuracy = make_tag("accuracy");
inline constexpr auto closure = make_tag("closure");
inline constexpr auto kernels = make_tag("kernels");
inline constexpr auto surface = make_tag("surface");
inline constexpr auto model = make_tag("model");
inline constexpr auto field = sub_tag(mesh & physics, "field");
inline constexpr auto field_dof = field & make_tag("dof");
inline constexpr auto field_dof_unit = field_dof & unit;
inline constexpr auto field_dof_integration = field_dof & integration;
inline constexpr auto field = sub_tag(mesh & physics, "field");
inline constexpr auto field_dof = field & make_tag("dof");
inline constexpr auto field_dof_unit = field_dof & unit;
inline constexpr auto field_dof_integration = field_dof & integration;
inline constexpr auto pressure = sub_tag(physics, "pressure");
inline constexpr auto pressure = sub_tag(physics, "pressure");
inline constexpr auto hydro = sub_tag(physics, "hydro");
inline constexpr auto jacobian = sub_tag(integration & physics, "jacobian");
inline constexpr auto residuals = sub_tag(integration & physics, "residuals");
inline constexpr auto volume = sub_tag(mesh & geometry, "volume");
inline constexpr auto quadrature =
sub_tag(mesh & geometry & solver, "quadrature");
inline constexpr auto convergence = sub_tag(solver, "convergence");
inline constexpr auto transformations =
sub_tag(mesh & geometry, "transformations");
inline constexpr auto hydro = sub_tag(physics, "hydro");
inline constexpr auto jacobian = sub_tag(integration & physics, "jacobian");
inline constexpr auto residuals = sub_tag(integration & physics, "residuals");
inline constexpr auto volume = sub_tag(mesh & geometry, "volume");
inline constexpr auto quadrature = sub_tag(mesh & geometry & solver, "quadrature");
inline constexpr auto convergence = sub_tag(solver, "convergence");
inline constexpr auto transformations = sub_tag(mesh & geometry, "transformations");
inline constexpr auto h_refinement =
sub_tag(mesh & convergence, "h_refinement");
inline constexpr auto p_refinement =
sub_tag(mesh & convergence, "p_refinement");
inline constexpr auto h_refinement = sub_tag(mesh & convergence, "h_refinement");
inline constexpr auto p_refinement = sub_tag(mesh & convergence, "p_refinement");
inline constexpr auto analytic_comparison =
sub_tag(solver & physics & residuals, "analytic_comparison");
inline constexpr auto self_consistency =
sub_tag(solver & physics, "self_consistency");
inline constexpr auto analytic_comparison = sub_tag(solver & physics & residuals, "analytic_comparison");
inline constexpr auto self_consistency = sub_tag(solver & physics, "self_consistency");
inline constexpr auto centrifugal = sub_tag(solver & physics, "centrifugal");
inline constexpr auto advection = sub_tag(solver & physics, "advection");
inline constexpr auto coriolis = sub_tag(solver & physics, "coriolis");
inline constexpr auto gravity = sub_tag(solver & physics, "gravity");
inline constexpr auto enthalpy = sub_tag(solver & physics, "enthalpy");
inline constexpr auto barotrope = sub_tag(physics, "barotrope");
inline constexpr auto mass_continuity =
sub_tag(solver & physics, "mass_continuity");
inline constexpr auto pressure_gradient =
sub_tag(solver & physics, "pressure_gradient");
inline constexpr auto viscosity = sub_tag(solver & physics, "viscosity");
inline constexpr auto centrifugal = sub_tag(solver & physics, "centrifugal");
inline constexpr auto advection = sub_tag(solver & physics, "advection");
inline constexpr auto coriolis = sub_tag(solver & physics, "coriolis");
inline constexpr auto gravity = sub_tag(solver & physics, "gravity");
inline constexpr auto enthalpy = sub_tag(solver & physics, "enthalpy");
inline constexpr auto barotrope = sub_tag(physics, "barotrope");
inline constexpr auto mass_continuity = sub_tag(solver & physics, "mass_continuity");
inline constexpr auto pressure_gradient = sub_tag(solver & physics, "pressure_gradient");
inline constexpr auto viscosity = sub_tag(solver & physics, "viscosity");
inline constexpr auto compactification =
sub_tag(mesh & mapping, "compactification");
inline constexpr auto kelvin = sub_tag(compactification, "kelvin");
inline constexpr auto mapping_evaluator =
mapping & make_tag("grid_function_evaluator");
inline constexpr auto mapping_evaluator_unit = mapping_evaluator & unit;
inline constexpr auto compactification = sub_tag(mesh & mapping, "compactification");
inline constexpr auto kelvin = sub_tag(compactification, "kelvin");
inline constexpr auto mapping_evaluator = mapping & make_tag("grid_function_evaluator");
inline constexpr auto mapping_evaluator_unit = mapping_evaluator & unit;
inline constexpr auto prepared = sub_tag(solver & physics, "prepared");
inline constexpr auto contexts = sub_tag(solver, "contexts");
inline constexpr auto prepared = sub_tag(solver & physics, "prepared");
inline constexpr auto contexts = sub_tag(solver, "contexts");
inline constexpr auto domain = sub_tag(mesh, "domain");
inline constexpr auto domain = sub_tag(mesh, "domain");
// Canonical gravity-suite tags. These intentionally compose leaf tags
// exactly once so Catch2 output remains useful and free of repeated
// [solver]/[physics] entries inherited from older composite tags.
inline constexpr auto gravity_unit = gravity & unit;
inline constexpr auto gravity_integration = gravity & integration;
inline constexpr auto gravity_operator = gravity & mfem_operators;
inline constexpr auto gravity_prepared = gravity & make_tag("prepared");
inline constexpr auto gravity_context = gravity & make_tag("context");
inline constexpr auto gravity_kernel = gravity & kernels;
inline constexpr auto gravity_accuracy = gravity & accuracy;
inline constexpr auto gravity_operator_unit = gravity_operator & unit;
inline constexpr auto gravity_operator_integration =
gravity_operator & integration;
inline constexpr auto gravity_operator_convergence =
gravity_operator & integration & make_tag("convergence");
inline constexpr auto gravity_analytic =
gravity & integration & make_tag("analytic_comparison");
inline constexpr auto gravity_consistency =
gravity & integration & make_tag("self_consistency");
inline constexpr auto gravity_prepared_jacobian =
gravity_prepared & integration & make_tag("jacobian");
inline constexpr auto gravity_prepared_unit = gravity_prepared & unit;
inline constexpr auto gravity_prepared_jacobian_accuracy =
gravity_prepared_jacobian & accuracy;
inline constexpr auto gravity_kernel_accuracy = gravity_kernel & accuracy;
inline constexpr auto gravity_kernel_integration = gravity_kernel & integration;
inline constexpr auto gravity_kernel_convergence =
gravity_kernel & integration & make_tag("convergence");
inline constexpr auto gravity_analytic_accuracy = gravity_analytic & accuracy;
inline constexpr auto gravity_consistency_accuracy =
gravity_consistency & accuracy;
inline constexpr auto gravity_integrator_unit = gravity & integrator & unit;
// Canonical gravity-suite tags. These intentionally compose leaf tags
// exactly once so Catch2 output remains useful and free of repeated
// [solver]/[physics] entries inherited from older composite tags.
inline constexpr auto gravity_unit = gravity & unit;
inline constexpr auto gravity_integration = gravity & integration;
inline constexpr auto gravity_operator = gravity & mfem_operators;
inline constexpr auto gravity_prepared = gravity & make_tag("prepared");
inline constexpr auto gravity_context = gravity & make_tag("context");
inline constexpr auto gravity_kernel = gravity & kernels;
inline constexpr auto gravity_accuracy = gravity & accuracy;
inline constexpr auto gravity_operator_unit = gravity_operator & unit;
inline constexpr auto gravity_operator_integration = gravity_operator & integration;
inline constexpr auto gravity_operator_convergence = gravity_operator & integration & make_tag("convergence");
inline constexpr auto gravity_analytic = gravity & integration & make_tag("analytic_comparison");
inline constexpr auto gravity_consistency = gravity & integration & make_tag("self_consistency");
inline constexpr auto gravity_prepared_jacobian = gravity_prepared & integration & make_tag("jacobian");
inline constexpr auto gravity_prepared_unit = gravity_prepared & unit;
inline constexpr auto gravity_prepared_jacobian_accuracy = gravity_prepared_jacobian & accuracy;
inline constexpr auto gravity_kernel_accuracy = gravity_kernel & accuracy;
inline constexpr auto gravity_kernel_integration = gravity_kernel & integration;
inline constexpr auto gravity_kernel_convergence = gravity_kernel & integration & make_tag("convergence");
inline constexpr auto gravity_analytic_accuracy = gravity_analytic & accuracy;
inline constexpr auto gravity_consistency_accuracy = gravity_consistency & accuracy;
inline constexpr auto gravity_integrator_unit = gravity & integrator & unit;
inline constexpr auto barotrope_prepared =
barotrope & solver & make_tag("prepared");
inline constexpr auto barotrope_eos_unit = barotrope & unit & make_tag("eos");
inline constexpr auto barotrope_eos_jacobian =
barotrope_eos_unit & integration & make_tag("jacobian");
inline constexpr auto barotrope_pressure_quadrature =
barotrope & mesh & geometry & solver & make_tag("pressure") &
make_tag("pressure_gradient") & make_tag("quadrature");
inline constexpr auto barotrope_pressure_quadrature_unit =
barotrope_pressure_quadrature & unit;
inline constexpr auto barotrope_pressure_quadrature_accuracy =
barotrope_pressure_quadrature & accuracy;
inline constexpr auto barotrope_prepared_jacobian =
barotrope_prepared & integration & make_tag("jacobian");
inline constexpr auto barotrope_context =
barotrope & solver & make_tag("context");
inline constexpr auto barotrope_context_integration =
barotrope_context & integration;
inline constexpr auto barotrope_prepared_analytic =
barotrope_prepared & integration & make_tag("analytic_comparison");
inline constexpr auto barotrope_prepared_jacobian_accuracy =
barotrope_prepared_jacobian & accuracy;
inline constexpr auto barotrope_prepared_jacobian_geometry =
barotrope_prepared_jacobian & geometry;
inline constexpr auto barotrope_prepared_jacobian_unit =
barotrope_prepared_jacobian & unit;
inline constexpr auto barotrope_prepared = barotrope & solver & make_tag("prepared");
inline constexpr auto barotrope_eos_unit = barotrope & unit & make_tag("eos");
inline constexpr auto barotrope_eos_jacobian = barotrope_eos_unit & integration & make_tag("jacobian");
inline constexpr auto polytropic_eos_characterization =
barotrope & unit & make_tag("eos") & make_tag("characterization");
inline constexpr auto polytropic_eos_relation_contract =
barotrope & unit & make_tag("eos") & make_tag("relation_contract");
inline constexpr auto polytropic_eos_compatibility = barotrope & unit & make_tag("eos") & make_tag("compatibility");
inline constexpr auto equation_of_state = physics & make_tag("eos");
inline constexpr auto equation_of_state_type_system = equation_of_state & unit & make_tag("type_system");
inline constexpr auto equation_of_state_quantity_types = equation_of_state_type_system & make_tag("quantity_types");
inline constexpr auto equation_of_state_relation_contract =
equation_of_state_type_system & make_tag("relation_contract");
inline constexpr auto equation_of_state_runtime_view = equation_of_state & unit & make_tag("runtime_view");
inline constexpr auto equation_of_state_runtime_contract =
equation_of_state_runtime_view & make_tag("relation_contract");
inline constexpr auto equation_of_state_runtime_compatibility =
equation_of_state_runtime_view & make_tag("compatibility");
inline constexpr auto equation_of_state_consumer_contract =
equation_of_state & unit & make_tag("consumer_contract");
inline constexpr auto barotropic_closure_equation_of_state_contract =
equation_of_state_consumer_contract & make_tag("barotropic_closure");
inline constexpr auto pressure_force_equation_of_state_contract =
equation_of_state_consumer_contract & make_tag("pressure_force");
inline constexpr auto structure_seed_equation_of_state_contract =
equation_of_state_consumer_contract & make_tag("structure_seed");
inline constexpr auto stellar_model_type_contract = barotrope & model & unit & make_tag("type_contract");
inline constexpr auto stellar_model_runtime_view = barotrope & model & unit & make_tag("runtime_view");
inline constexpr auto surface_prescription_type_contract =
surface & physics & unit & make_tag("prescription") & make_tag("type_contract");
inline constexpr auto surface_constraint_compilation =
surface & physics & unit & make_tag("constraint_compilation");
inline constexpr auto surface_constraint_jacobian = surface_constraint_compilation & jacobian;
inline constexpr auto surface_constraint_lifetime = surface & model & unit & make_tag("constraint_lifetime");
inline constexpr auto surface_boundary_dof_topology =
surface & field_dof & integration & make_tag("boundary_topology");
inline constexpr auto surface_row_replacement =
surface & barotrope_prepared & integration & make_tag("row_replacement");
inline constexpr auto translational_centering = geometry & solver & make_tag("translational_centering");
inline constexpr auto translational_centering_topology =
translational_centering & field_dof & integration & make_tag("point_topology");
inline constexpr auto translational_centering_enforcement =
translational_centering & barotrope_prepared & integration & make_tag("row_replacement");
inline constexpr auto barotrope_pressure_quadrature = barotrope & mesh & geometry & solver & make_tag("pressure") &
make_tag("pressure_gradient") & make_tag("quadrature");
inline constexpr auto barotrope_pressure_quadrature_unit = barotrope_pressure_quadrature & unit;
inline constexpr auto barotrope_pressure_quadrature_accuracy = barotrope_pressure_quadrature & accuracy;
inline constexpr auto barotrope_prepared_jacobian = barotrope_prepared & integration & make_tag("jacobian");
inline constexpr auto barotrope_context = barotrope & solver & make_tag("context");
inline constexpr auto barotrope_context_integration = barotrope_context & integration;
inline constexpr auto barotrope_prepared_analytic =
barotrope_prepared & integration & make_tag("analytic_comparison");
inline constexpr auto barotrope_prepared_jacobian_accuracy = barotrope_prepared_jacobian & accuracy;
inline constexpr auto barotrope_prepared_jacobian_geometry = barotrope_prepared_jacobian & geometry;
inline constexpr auto barotrope_prepared_jacobian_unit = barotrope_prepared_jacobian & unit;
// Canonical hydrostatic-suite tags. The leaf tags are composed directly
// so inherited [physics]/[solver] tags appear only once.
inline constexpr auto barotrope_hydrostatic =
barotrope & solver & make_tag("hydro");
inline constexpr auto barotrope_hydrostatic_context =
barotrope_hydrostatic & make_tag("context");
inline constexpr auto barotrope_hydrostatic_prepared =
barotrope_hydrostatic & make_tag("prepared");
inline constexpr auto barotrope_hydrostatic_prepared_residual =
barotrope_hydrostatic_prepared & integration & make_tag("residual");
inline constexpr auto barotrope_hydrostatic_prepared_jacobian =
barotrope_hydrostatic_prepared & integration & make_tag("jacobian");
inline constexpr auto barotrope_hydrostatic_prepared_analytic =
barotrope_hydrostatic_prepared & integration &
make_tag("analytic_comparison");
// Canonical hydrostatic-suite tags. The leaf tags are composed directly
// so inherited [physics]/[solver] tags appear only once.
inline constexpr auto barotrope_hydrostatic = barotrope & solver & make_tag("hydro");
inline constexpr auto barotrope_hydrostatic_context = barotrope_hydrostatic & make_tag("context");
inline constexpr auto barotrope_hydrostatic_prepared = barotrope_hydrostatic & make_tag("prepared");
inline constexpr auto barotrope_hydrostatic_prepared_residual =
barotrope_hydrostatic_prepared & integration & make_tag("residual");
inline constexpr auto barotrope_hydrostatic_prepared_jacobian =
barotrope_hydrostatic_prepared & integration & make_tag("jacobian");
inline constexpr auto barotrope_hydrostatic_prepared_analytic =
barotrope_hydrostatic_prepared & integration & make_tag("analytic_comparison");
inline constexpr auto barotrope_mass_normalization =
barotrope & solver & make_tag("mass_normalization");
inline constexpr auto barotrope_mass_normalization_context =
barotrope_mass_normalization & make_tag("context");
inline constexpr auto barotrope_mass_normalization_prepared =
barotrope_mass_normalization & make_tag("prepared");
inline constexpr auto barotrope_mass_normalization_jacobian =
barotrope_mass_normalization_prepared & integration & make_tag("jacobian");
inline constexpr auto barotrope_mass_normalization_analytic =
barotrope_mass_normalization_prepared & integration &
make_tag("analytic_comparison");
inline constexpr auto barotrope_mass_normalization = barotrope & solver & make_tag("mass_normalization");
inline constexpr auto barotrope_mass_normalization_context = barotrope_mass_normalization & make_tag("context");
inline constexpr auto barotrope_mass_normalization_prepared = barotrope_mass_normalization & make_tag("prepared");
inline constexpr auto barotrope_mass_normalization_jacobian =
barotrope_mass_normalization_prepared & integration & make_tag("jacobian");
inline constexpr auto barotrope_mass_normalization_analytic =
barotrope_mass_normalization_prepared & integration & make_tag("analytic_comparison");
inline constexpr auto rotation_prepared = centrifugal & make_tag("prepared");
inline constexpr auto rotation_context = centrifugal & make_tag("context");
inline constexpr auto rotation_analytic =
centrifugal & integration & make_tag("analytic_comparison");
inline constexpr auto rotation_context_unit = rotation_context & unit;
inline constexpr auto rotation_prepared_unit = rotation_prepared & unit;
inline constexpr auto rotation_prepared_jacobian =
rotation_prepared & integration & make_tag("jacobian");
inline constexpr auto rotation_prepared_jacobian_accuracy =
rotation_prepared_jacobian & accuracy;
inline constexpr auto rotation_kernel_accuracy =
centrifugal & kernels & accuracy;
inline constexpr auto rotation_integrator_unit = centrifugal & integrator & unit;
inline constexpr auto rotation_integrator_integration =
centrifugal & integrator & integration;
inline constexpr auto rotation_integrator_convergence =
rotation_integrator_integration & convergence & h_refinement;
inline constexpr auto rotation_analytic_unit = rotation_analytic & unit;
inline constexpr auto rotation_analytic_accuracy = rotation_analytic & accuracy;
inline constexpr auto rotation_analytic_accuracy_geometry =
rotation_analytic_accuracy & geometry;
inline constexpr auto rotation_prepared = centrifugal & make_tag("prepared");
inline constexpr auto rotation_context = centrifugal & make_tag("context");
inline constexpr auto rotation_analytic = centrifugal & integration & make_tag("analytic_comparison");
inline constexpr auto rotation_context_unit = rotation_context & unit;
inline constexpr auto rotation_prepared_unit = rotation_prepared & unit;
inline constexpr auto rotation_prepared_jacobian = rotation_prepared & integration & make_tag("jacobian");
inline constexpr auto rotation_prepared_jacobian_accuracy = rotation_prepared_jacobian & accuracy;
inline constexpr auto rotation_kernel_accuracy = centrifugal & kernels & accuracy;
inline constexpr auto rotation_integrator_unit = centrifugal & integrator & unit;
inline constexpr auto rotation_integrator_integration = centrifugal & integrator & integration;
inline constexpr auto rotation_integrator_convergence =
rotation_integrator_integration & convergence & h_refinement;
inline constexpr auto rotation_analytic_unit = rotation_analytic & unit;
inline constexpr auto rotation_analytic_accuracy = rotation_analytic & accuracy;
inline constexpr auto rotation_analytic_accuracy_geometry = rotation_analytic_accuracy & geometry;
} // namespace tags

View File

@@ -3,6 +3,7 @@
#include <catch2/catch_test_case_info.hpp>
#include <catch2/reporters/catch_reporter_registrars.hpp>
#include <catch2/reporters/catch_reporter_streaming_base.hpp>
#include <chrono>
#include <fstream>
#include <iomanip>
#include <iostream>
@@ -223,6 +224,7 @@ class CheckReporter : public Catch::StreamingReporterBase {
bool passed;
std::size_t assertionsPassed;
std::size_t assertionsFailed;
double durationSeconds;
std::vector<std::string> failureMessages;
std::vector<std::string> infoMessages;
};
@@ -231,6 +233,7 @@ class CheckReporter : public Catch::StreamingReporterBase {
std::vector<std::string> m_currentInfos;
std::unordered_set<unsigned int> m_currentInfoSequences;
std::vector<TestCaseData> m_testRunData;
std::chrono::time_point<std::chrono::steady_clock> m_testStartTime;
void captureInfoMessages(Catch::AssertionStats const &assertionStats) {
for (auto const &message : assertionStats.infoMessages) {
@@ -253,9 +256,8 @@ public:
}
static std::string getDescription() {
return "Console reporter with wrapping, tags, and collapsible HTML "
"export "
"with ANSI color rendering.";
return "Console reporter with wrapping, tags, live test progress, and collapsible HTML "
"export with ANSI color rendering.";
}
void testRunStarting(Catch::TestRunInfo const &_testRunInfo) override {
@@ -263,8 +265,20 @@ public:
std::cout << '\n';
std::cout << std::left << std::setw(85) << "Test Case Name"
<< "Status " << std::right << std::setw(8) << "Passed" << std::setw(8) << "Failed" << '\n';
std::cout << std::string(121, '-') << '\n';
<< "Status " << std::right << std::setw(8) << "Passed" << std::setw(8) << "Failed" << std::setw(12)
<< "Time (s)" << '\n';
std::cout << std::string(133, '-') << '\n';
}
void testCaseStarting(Catch::TestCaseInfo const &testInfo) override {
StreamingReporterBase::testCaseStarting(testInfo);
m_testStartTime = std::chrono::steady_clock::now();
std::string name = testInfo.name;
auto wrappedName = wrapText(name, 83);
// Print progress line, \r to overwrite later, \033[K to clear till end of line
std::cout << "\r\033[K" << std::left << std::setw(85) << (wrappedName[0] + " ...") << std::flush;
}
void assertionEnded(Catch::AssertionStats const &assertionStats) override {
@@ -300,14 +314,20 @@ public:
void testCaseEnded(Catch::TestCaseStats const &stats) override {
StreamingReporterBase::testCaseEnded(stats);
bool passed = stats.totals.assertions.allPassed();
std::string mark = passed ? "\033[32m✓\033[0m" : "\033[31m✗\033[0m";
auto endTime = std::chrono::steady_clock::now();
std::chrono::duration<double> elapsed = endTime - m_testStartTime;
double duration_s = elapsed.count();
std::string name = stats.testInfo->name;
auto wrappedName = wrapText(name, 83);
bool passed = stats.totals.assertions.allPassed();
std::string mark = passed ? "\033[32m✓\033[0m" : "\033[31m✗\033[0m";
std::cout << std::left << std::setw(85) << wrappedName[0] << mark << " " << std::right << std::setw(8)
<< stats.totals.assertions.passed << std::setw(8) << stats.totals.assertions.failed << '\n';
std::string name = stats.testInfo->name;
auto wrappedName = wrapText(name, 83);
// Overwrite the loading line with the actual result
std::cout << "\r\033[K" << std::left << std::setw(85) << wrappedName[0] << mark << " " << std::right
<< std::setw(8) << stats.totals.assertions.passed << std::setw(8) << stats.totals.assertions.failed
<< std::setw(11) << std::fixed << std::setprecision(3) << duration_s << "s\n";
for (size_t i = 1; i < wrappedName.size(); ++i) {
std::cout << " \033[90m↳ \033[0m" // Dim indent arrow
@@ -327,12 +347,12 @@ public:
for (auto const &failure : m_currentFailures) {
std::cout << failure << '\n';
}
std::cout << std::string(121, '-') << '\n';
std::cout << std::string(133, '-') << '\n';
}
m_testRunData.push_back(
{name, tagsStr, passed, stats.totals.assertions.passed, stats.totals.assertions.failed, m_currentFailures,
m_currentInfos}
{name, tagsStr, passed, stats.totals.assertions.passed, stats.totals.assertions.failed, duration_s,
m_currentFailures, m_currentInfos}
);
m_currentFailures.clear();
@@ -343,7 +363,7 @@ public:
void testRunEnded(Catch::TestRunStats const &_testRunStats) override {
StreamingReporterBase::testRunEnded(_testRunStats);
std::cout << std::string(121, '=') << '\n';
std::cout << std::string(133, '=') << '\n';
auto const &tc = _testRunStats.totals.testCases;
auto const &as = _testRunStats.totals.assertions;
@@ -444,7 +464,9 @@ private:
html << " </div>\n";
html << " <div class='stats'>\n";
html << " <span class='text-green'>&#10003; " << test.assertionsPassed << "</span> | ";
html << " <span class='text-red'>&#10007; " << test.assertionsFailed << "</span>\n";
html << " <span class='text-red'>&#10007; " << test.assertionsFailed << "</span> | ";
html << " <span style='color: #34495e;'>&#8987; " << std::fixed << std::setprecision(3)
<< test.durationSeconds << "s</span>\n";
html << " </div>\n";
html << " </div>\n";
@@ -589,4 +611,4 @@ int main(
test_utils::set_args(std::move(test_args));
return session.run();
}
}

File diff suppressed because it is too large Load Diff