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

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

414
tests/field/field_base.cpp Normal file
View File

@@ -0,0 +1,414 @@
#include <catch2/catch_test_macros.hpp>
#include <concepts>
#include <cstddef>
#include <string_view>
#include <type_traits>
import mean_field;
import test_helpers;
namespace field_base_test_utils {
namespace field = mean_field::field;
namespace domain = mean_field::utils::domain;
using IndependentL2Scalar = field::ScalarQ<field::FieldRelation::Independent, field::Disc<field::L2, 2>>;
using IndependentH1Scalar = field::ScalarQ<field::FieldRelation::Independent, field::Disc<field::H1, 3>>;
using IndependentH1Vector = field::VectorQ<field::FieldRelation::Independent, field::Disc<field::H1, 3>>;
using IndependentL2Vector = field::VectorQ<field::FieldRelation::Independent, field::Disc<field::L2, 2>>;
using Potential = field::ScalarQ<field::FieldRelation::Independent, field::Disc<field::L2, 2>>;
using Flux = field::VectorQ<field::FieldRelation::Gradient<Potential>, field::Disc<field::RT, 2>>;
using CurlSource = field::VectorQ<field::FieldRelation::Independent, field::Disc<field::ND, 2>>;
using CurlQuantity = field::VectorQ<field::FieldRelation::Curl<CurlSource>, field::Disc<field::ND, 2>>;
using SampleOperand = field::Operand<IndependentH1Scalar>;
using SampleGradientOperand = field::Operand<IndependentH1Scalar, field::FieldOperation::Gradient>;
using SampleForm = field::FormSpec<17, 2, SampleOperand, SampleGradientOperand>;
struct StellarSupportedObject {
using Support = field::DomainSupport<domain::Stellar>;
};
struct AllSupportedObject {
using Support = field::DomainSupport<domain::All>;
};
struct NonSpatialObject {
using Support = field::NonSpatialSupport;
};
} // namespace field_base_test_utils
TEST_CASE(
"Field Base Type Lists Track Compile Time Membership",
tags::unit &tags::field
) {
namespace field = mean_field::field;
using List = field::TypeList<int, double, char>;
STATIC_REQUIRE(field::typeListContains<int, List>);
STATIC_REQUIRE(field::typeListContains<double, List>);
STATIC_REQUIRE(field::typeListContains<char, List>);
STATIC_REQUIRE_FALSE(field::typeListContains<float, List>);
STATIC_REQUIRE_FALSE(field::typeListContains<int, field::TypeList<>>);
CHECK(true);
}
TEST_CASE(
"Field Base Function Space Tags Encode Supported Tensor Ranks",
tags::unit &tags::field
) {
namespace field = mean_field::field;
STATIC_REQUIRE(field::SpaceTag<field::L2>);
STATIC_REQUIRE(field::SpaceTag<field::H1>);
STATIC_REQUIRE(field::SpaceTag<field::RT>);
STATIC_REQUIRE(field::SpaceTag<field::ND>);
STATIC_REQUIRE_FALSE(field::SpaceTag<int>);
STATIC_REQUIRE(field::spaceSupportsRank<field::L2, 0>);
STATIC_REQUIRE(field::spaceSupportsRank<field::L2, 1>);
STATIC_REQUIRE(field::spaceSupportsRank<field::H1, 0>);
STATIC_REQUIRE(field::spaceSupportsRank<field::H1, 1>);
STATIC_REQUIRE_FALSE(field::spaceSupportsRank<field::RT, 0>);
STATIC_REQUIRE(field::spaceSupportsRank<field::RT, 1>);
STATIC_REQUIRE_FALSE(field::spaceSupportsRank<field::ND, 0>);
STATIC_REQUIRE(field::spaceSupportsRank<field::ND, 1>);
CHECK(field::L2::name == std::string_view{"L2"});
CHECK(field::H1::name == std::string_view{"H1"});
CHECK(field::RT::name == std::string_view{"RT"});
CHECK(field::ND::name == std::string_view{"ND"});
}
TEST_CASE(
"Field Base Discretization Descriptors Preserve Space And Family Order",
tags::unit &tags::field
) {
namespace field = mean_field::field;
using L2Disc = field::Disc<field::L2, 2>;
using H1Disc = field::Disc<field::H1, 4>;
using RTDisc = field::Disc<field::RT, 1>;
using NDDisc = field::Disc<field::ND, 3>;
STATIC_REQUIRE(field::DiscretizationTag<L2Disc>);
STATIC_REQUIRE(field::DiscretizationTag<H1Disc>);
STATIC_REQUIRE(field::DiscretizationTag<RTDisc>);
STATIC_REQUIRE(field::DiscretizationTag<NDDisc>);
STATIC_REQUIRE(std::same_as<typename L2Disc::Space, field::L2>);
STATIC_REQUIRE(std::same_as<typename H1Disc::Space, field::H1>);
STATIC_REQUIRE(L2Disc::familyOrder == 2);
STATIC_REQUIRE(H1Disc::familyOrder == 4);
STATIC_REQUIRE(RTDisc::familyOrder == 1);
STATIC_REQUIRE(NDDisc::familyOrder == 3);
CHECK(true);
}
TEST_CASE(
"Field Base Relations Preserve Their Source Quantities",
tags::unit &tags::field
) {
namespace field = mean_field::field;
using Source = field_base_test_utils::IndependentL2Scalar;
using Gradient = field::FieldRelation::Gradient<Source>;
using Divergence = field::FieldRelation::Divergence<Source>;
using Curl = field::FieldRelation::Curl<Source>;
STATIC_REQUIRE(field::ValidRelation<field::FieldRelation::Independent>);
STATIC_REQUIRE(field::ValidRelation<Gradient>);
STATIC_REQUIRE(field::ValidRelation<Divergence>);
STATIC_REQUIRE(field::ValidRelation<Curl>);
STATIC_REQUIRE_FALSE(field::ValidRelation<int>);
STATIC_REQUIRE(field::IsGradient<Gradient>::value);
STATIC_REQUIRE(field::IsDivergence<Divergence>::value);
STATIC_REQUIRE(field::IsCurl<Curl>::value);
STATIC_REQUIRE(std::same_as<typename field::RelationTarget<Gradient>::Type, Source>);
STATIC_REQUIRE(std::same_as<typename field::RelationTarget<Divergence>::Type, Source>);
STATIC_REQUIRE(std::same_as<typename field::RelationTarget<Curl>::Type, Source>);
STATIC_REQUIRE(std::same_as<typename field::RelationTarget<field::FieldRelation::Independent>::Type, void>);
CHECK(true);
}
TEST_CASE(
"Field Base Finite Element Quantities Preserve Rank Storage Space And Order",
tags::unit &tags::field
) {
namespace field = mean_field::field;
using Scalar = field_base_test_utils::IndependentL2Scalar;
using Vector = field_base_test_utils::IndependentH1Vector;
STATIC_REQUIRE(field::FieldQuantity<Scalar>);
STATIC_REQUIRE(field::FieldQuantity<Vector>);
STATIC_REQUIRE(field::RegisteredQuantity<Scalar>);
STATIC_REQUIRE(field::RegisteredQuantity<Vector>);
STATIC_REQUIRE(Scalar::rankValue == 0);
STATIC_REQUIRE(Vector::rankValue == 1);
STATIC_REQUIRE(Scalar::familyOrder == 2);
STATIC_REQUIRE(Vector::familyOrder == 3);
STATIC_REQUIRE(std::same_as<typename Scalar::Space, field::L2>);
STATIC_REQUIRE(std::same_as<typename Vector::Space, field::H1>);
STATIC_REQUIRE(Scalar::storageKind == field::StorageKind::finite_element);
STATIC_REQUIRE(Vector::storageKind == field::StorageKind::finite_element);
STATIC_REQUIRE(Scalar::staticBlockSize == field::dynamicBlockSize);
STATIC_REQUIRE(Vector::staticBlockSize == field::dynamicBlockSize);
CHECK(true);
}
TEST_CASE(
"Field Base Global Scalars Are Registered But Are Not Finite Element Quantities",
tags::unit &tags::field
) {
namespace field = mean_field::field;
STATIC_REQUIRE(field::GlobalScalarQuantity<field::GlobalScalarQ>);
STATIC_REQUIRE(field::RegisteredQuantity<field::GlobalScalarQ>);
STATIC_REQUIRE_FALSE(field::FieldQuantity<field::GlobalScalarQ>);
STATIC_REQUIRE(field::GlobalScalarQ::rankValue == 0);
STATIC_REQUIRE(field::GlobalScalarQ::storageKind == field::StorageKind::global_scalar);
STATIC_REQUIRE(field::GlobalScalarQ::staticBlockSize == 1);
STATIC_REQUIRE(std::same_as<typename field::GlobalScalarQ::Relation, field::FieldRelation::Independent>);
CHECK(true);
}
TEST_CASE(
"Field Base Derived Quantity Detection Follows Physical Relations",
tags::unit &tags::field
) {
namespace field = mean_field::field;
using Independent = field_base_test_utils::IndependentL2Scalar;
using Flux = field_base_test_utils::Flux;
using CurlQuantity = field_base_test_utils::CurlQuantity;
STATIC_REQUIRE_FALSE(field::DerivedQuantity<Independent>);
STATIC_REQUIRE(field::DerivedQuantity<Flux>);
STATIC_REQUIRE(field::DerivedQuantity<CurlQuantity>);
STATIC_REQUIRE(std::same_as<field::RelationTargetT<Flux>, field_base_test_utils::Potential>);
STATIC_REQUIRE(std::same_as<field::RelationTargetT<CurlQuantity>, field_base_test_utils::CurlSource>);
CHECK(true);
}
TEST_CASE(
"Field Base RT L2 Constraint Accepts The Registered Stable Pair Contract",
tags::unit &tags::field
) {
namespace field = mean_field::field;
using Potential = field_base_test_utils::Potential;
using Flux = field_base_test_utils::Flux;
using Constraint = field::RtL2StablePair<Flux, Potential>;
STATIC_REQUIRE(field::validate_constraints(field::TypeList<Constraint>{}));
STATIC_REQUIRE(field::validate_constraints(field::TypeList<>{}));
CHECK(true);
}
TEST_CASE(
"Field Base Operations And Operands Preserve Mathematical Intent",
tags::unit &tags::field
) {
namespace field = mean_field::field;
using Quantity = field_base_test_utils::IndependentH1Scalar;
using ValueOperand = field::Operand<Quantity>;
using GradientOperand = field::Operand<Quantity, field::FieldOperation::Gradient>;
STATIC_REQUIRE(field::FieldOperationTag<field::FieldOperation::Value>);
STATIC_REQUIRE(field::FieldOperationTag<field::FieldOperation::Gradient>);
STATIC_REQUIRE(field::FieldOperationTag<field::FieldOperation::Divergence>);
STATIC_REQUIRE(field::FieldOperationTag<field::FieldOperation::Curl>);
STATIC_REQUIRE(field::FieldOperationTag<field::FieldOperation::NormalTrace>);
STATIC_REQUIRE_FALSE(field::FieldOperationTag<int>);
STATIC_REQUIRE(field::FieldOperand<ValueOperand>);
STATIC_REQUIRE(field::FieldOperand<GradientOperand>);
STATIC_REQUIRE(std::same_as<typename ValueOperand::Quantity, Quantity>);
STATIC_REQUIRE(std::same_as<typename ValueOperand::Operation, field::FieldOperation::Value>);
STATIC_REQUIRE(std::same_as<typename GradientOperand::Operation, field::FieldOperation::Gradient>);
CHECK(true);
}
TEST_CASE(
"Field Base Form Specifications Preserve Policy Dynamic Orders And Operands",
tags::unit &tags::field
) {
namespace field = mean_field::field;
using Form = field_base_test_utils::SampleForm;
STATIC_REQUIRE(field::FieldForm<Form>);
STATIC_REQUIRE(Form::policyKey == 17);
STATIC_REQUIRE(Form::dynamicOrderCount == 2);
STATIC_REQUIRE(
std::same_as<
typename Form::Operands,
field::TypeList<field_base_test_utils::SampleOperand, field_base_test_utils::SampleGradientOperand>>
);
STATIC_REQUIRE(
field::isRegisteredQuantityList<field::TypeList<
field_base_test_utils::IndependentL2Scalar, field_base_test_utils::IndependentH1Vector,
field::GlobalScalarQ>>
);
STATIC_REQUIRE_FALSE(field::isRegisteredQuantityList<field::TypeList<int>>);
STATIC_REQUIRE(field::isFieldFormList<field::TypeList<Form>>);
STATIC_REQUIRE(field::isFieldFormList<field::TypeList<>>);
STATIC_REQUIRE_FALSE(field::isFieldFormList<field::TypeList<int>>);
CHECK(true);
}
TEST_CASE(
"Field Base Support Types Distinguish Domain And Non Spatial Fields",
tags::unit &tags::field
) {
namespace field = mean_field::field;
namespace domain = mean_field::utils::domain;
using StellarSupport = field::DomainSupport<domain::Stellar>;
using AllSupport = field::DomainSupport<domain::All>;
STATIC_REQUIRE(field::IsFieldSupport<StellarSupport>);
STATIC_REQUIRE(field::IsFieldSupport<AllSupport>);
STATIC_REQUIRE(field::IsFieldSupport<field::NonSpatialSupport>);
STATIC_REQUIRE(field::IsDomainSupport<StellarSupport>);
STATIC_REQUIRE(field::IsDomainSupport<AllSupport>);
STATIC_REQUIRE_FALSE(field::IsDomainSupport<field::NonSpatialSupport>);
STATIC_REQUIRE(std::same_as<typename StellarSupport::Domain, domain::Stellar>);
STATIC_REQUIRE(std::same_as<typename AllSupport::Domain, domain::All>);
STATIC_REQUIRE(field::DomainSupportedField<field_base_test_utils::StellarSupportedObject>);
STATIC_REQUIRE(field::DomainSupportedField<field_base_test_utils::AllSupportedObject>);
STATIC_REQUIRE_FALSE(field::DomainSupportedField<field_base_test_utils::NonSpatialObject>);
STATIC_REQUIRE(field::NonSpatialField<field_base_test_utils::NonSpatialObject>);
STATIC_REQUIRE_FALSE(field::NonSpatialField<field_base_test_utils::StellarSupportedObject>);
STATIC_REQUIRE(std::same_as<field::FieldSupportT<field_base_test_utils::StellarSupportedObject>, StellarSupport>);
STATIC_REQUIRE(std::same_as<field::FieldDomainT<field_base_test_utils::StellarSupportedObject>, domain::Stellar>);
CHECK(true);
}

View File

@@ -0,0 +1,724 @@
#include <algorithm>
#include <array>
#include <catch2/catch_test_macros.hpp>
#include <cstddef>
#include <mfem.hpp>
#include <mpi.h>
#include <optional>
#include <stdexcept>
#include <vector>
import mean_field;
import test_helpers;
namespace field_dof_map_test_utils {
namespace field = mean_field::field;
namespace domain = mean_field::utils::domain;
using Schema = domain::CoreEnvelopeVacuumDomainSchema;
[[nodiscard]]
mfem::Array<int> make_array(const std::initializer_list<int> values) {
mfem::Array<int> result(static_cast<int>(values.size()));
int index = 0;
for (const int value : values) {
result[index++] = value;
}
return result;
}
[[nodiscard]]
mfem::Mesh make_split_mesh(
const int stellarAttribute = 2,
const int vacuumAttribute = 3
) {
int communicatorSize = 1;
MPI_Comm_size(MPI_COMM_WORLD, &communicatorSize);
/*
* Ensure there are enough cells that every reasonable MPI test
* configuration has useful work available.
*/
const int xElementCount = std::max(4, 2 * communicatorSize);
constexpr int yElementCount = 2;
mfem::Mesh mesh = mfem::Mesh::MakeCartesian2D(
xElementCount, yElementCount, mfem::Element::QUADRILATERAL, true, static_cast<double>(xElementCount),
static_cast<double>(yElementCount)
);
for (int elementId = 0; elementId < mesh.GetNE(); ++elementId) {
const int xIndex = elementId % xElementCount;
mesh.GetElement(elementId)->SetAttribute(xIndex < xElementCount / 2 ? stellarAttribute : vacuumAttribute);
}
mesh.SetAttributes();
return mesh;
}
[[nodiscard]]
long long global_sum(const int localValue) {
const long long local = static_cast<long long>(localValue);
long long global = 0;
MPI_Allreduce(&local, &global, 1, MPI_LONG_LONG, MPI_SUM, MPI_COMM_WORLD);
return global;
}
template <typename FieldT>
concept CanMakeFieldDofMap =
requires(const mfem::ParFiniteElementSpace &space) { field::make_field_dof_map<FieldT, Schema>(space); };
using AlternateSchema = domain::DomainSchema<
domain::MaterialList<
domain::Material<domain::Core, 11>,
domain::Material<domain::Envelope, 17>,
domain::Material<domain::Vacuum, 29>>,
domain::BoundaryList<>,
domain::RelationList<>>;
} // namespace field_dof_map_test_utils
TEST_CASE(
"Field DOF Map Preserves Canonical Bidirectional Indexing",
tags::unit &tags::field
) {
namespace field = mean_field::field;
const mfem::Array<int> active = field_dof_map_test_utils::make_array({0, 2, 5, 7});
const field::FieldDofMap map(9, active);
CHECK(map.full_size() == 9);
CHECK(map.reduced_size() == 4);
CHECK(map.inactive_size() == 5);
CHECK_FALSE(map.is_identity());
CHECK(map.true_dof(0) == 0);
CHECK(map.true_dof(1) == 2);
CHECK(map.true_dof(2) == 5);
CHECK(map.true_dof(3) == 7);
REQUIRE(map.reduced_dof(0).has_value());
REQUIRE(map.reduced_dof(2).has_value());
REQUIRE(map.reduced_dof(5).has_value());
REQUIRE(map.reduced_dof(7).has_value());
CHECK(*map.reduced_dof(0) == 0);
CHECK(*map.reduced_dof(2) == 1);
CHECK(*map.reduced_dof(5) == 2);
CHECK(*map.reduced_dof(7) == 3);
CHECK_FALSE(map.reduced_dof(1).has_value());
CHECK_FALSE(map.reduced_dof(3).has_value());
CHECK(map.contains_true_dof(0));
CHECK(map.contains_true_dof(2));
CHECK_FALSE(map.contains_true_dof(1));
const mfem::Array<int> &forward = map.reduced_to_true();
const mfem::Array<int> &inverse = map.true_to_reduced();
REQUIRE(forward.Size() == 4);
REQUIRE(inverse.Size() == 9);
CHECK(forward[0] == 0);
CHECK(forward[1] == 2);
CHECK(forward[2] == 5);
CHECK(forward[3] == 7);
CHECK(inverse[0] == 0);
CHECK(inverse[1] == -1);
CHECK(inverse[2] == 1);
CHECK(inverse[3] == -1);
CHECK(inverse[4] == -1);
CHECK(inverse[5] == 2);
CHECK(inverse[6] == -1);
CHECK(inverse[7] == 3);
CHECK(inverse[8] == -1);
}
TEST_CASE(
"Field DOF Map Rejects Invalid Canonical Mappings",
tags::unit &tags::field
) {
namespace field = mean_field::field;
const mfem::Array<int> empty;
CHECK_THROWS_AS((field::FieldDofMap(-1, empty)), std::invalid_argument);
CHECK_THROWS_AS((field::FieldDofMap(4, field_dof_map_test_utils::make_array({-1, 2}))), std::invalid_argument);
CHECK_THROWS_AS((field::FieldDofMap(4, field_dof_map_test_utils::make_array({1, 4}))), std::invalid_argument);
/*
* Duplicate true DOF.
*/
CHECK_THROWS_AS((field::FieldDofMap(5, field_dof_map_test_utils::make_array({1, 1, 3}))), std::invalid_argument);
/*
* Non-canonical unsorted ordering.
*/
CHECK_THROWS_AS((field::FieldDofMap(5, field_dof_map_test_utils::make_array({1, 3, 2}))), std::invalid_argument);
}
TEST_CASE(
"Field DOF Map Rejects Out Of Range Index Queries",
tags::unit &tags::field
) {
namespace field = mean_field::field;
const field::FieldDofMap map(5, field_dof_map_test_utils::make_array({1, 3}));
CHECK_THROWS_AS(map.true_dof(-1), std::out_of_range);
CHECK_THROWS_AS(map.true_dof(2), std::out_of_range);
CHECK_THROWS_AS(map.reduced_dof(-1), std::out_of_range);
CHECK_THROWS_AS(map.reduced_dof(5), std::out_of_range);
CHECK_THROWS_AS(map.contains_true_dof(-1), std::out_of_range);
CHECK_THROWS_AS(map.contains_true_dof(5), std::out_of_range);
}
TEST_CASE(
"Field DOF Map Gather Selects Exactly The Active True DOFs",
tags::unit &tags::field
) {
namespace field = mean_field::field;
const field::FieldDofMap map(6, field_dof_map_test_utils::make_array({1, 3, 5}));
mfem::Vector full(6);
for (int trueDof = 0; trueDof < full.Size(); ++trueDof) {
full(trueDof) = 10.0 + static_cast<double>(trueDof);
}
const mfem::Vector reduced = map.gather(full);
REQUIRE(reduced.Size() == 3);
CHECK(reduced(0) == 11.0);
CHECK(reduced(1) == 13.0);
CHECK(reduced(2) == 15.0);
mfem::Vector output(3);
map.gather(full, output);
CHECK(output(0) == 11.0);
CHECK(output(1) == 13.0);
CHECK(output(2) == 15.0);
}
TEST_CASE(
"Field DOF Map Scatter Produces The Canonical Supported Projection",
tags::unit &tags::field
) {
namespace field = mean_field::field;
const field::FieldDofMap map(6, field_dof_map_test_utils::make_array({1, 3, 5}));
mfem::Vector reduced(3);
reduced(0) = 2.0;
reduced(1) = 4.0;
reduced(2) = 6.0;
const mfem::Vector full = map.scatter(reduced);
REQUIRE(full.Size() == 6);
CHECK(full(0) == 0.0);
CHECK(full(1) == 2.0);
CHECK(full(2) == 0.0);
CHECK(full(3) == 4.0);
CHECK(full(4) == 0.0);
CHECK(full(5) == 6.0);
const mfem::Vector roundTrip = map.gather(full);
REQUIRE(roundTrip.Size() == reduced.Size());
for (int index = 0; index < reduced.Size(); ++index) {
CHECK(roundTrip(index) == reduced(index));
}
}
TEST_CASE(
"Field DOF Map Gather Scatter Projects A Full Vector Onto Field Support",
tags::unit &tags::field
) {
namespace field = mean_field::field;
const field::FieldDofMap map(7, field_dof_map_test_utils::make_array({0, 2, 3, 6}));
mfem::Vector original(7);
for (int index = 0; index < original.Size(); ++index) {
original(index) = 0.25 + static_cast<double>(index);
}
const mfem::Vector reduced = map.gather(original);
const mfem::Vector projected = map.scatter(reduced);
for (int trueDof = 0; trueDof < original.Size(); ++trueDof) {
CAPTURE(trueDof);
if (map.contains_true_dof(trueDof)) {
CHECK(projected(trueDof) == original(trueDof));
} else {
CHECK(projected(trueDof) == 0.0);
}
}
}
TEST_CASE(
"Field DOF Map Scatter Into Preserves Unsupported True DOFs",
tags::unit &tags::field
) {
namespace field = mean_field::field;
const field::FieldDofMap map(6, field_dof_map_test_utils::make_array({1, 4}));
mfem::Vector reduced(2);
reduced(0) = 7.0;
reduced(1) = 9.0;
mfem::Vector full(6);
full = -3.0;
map.scatter_into(reduced, full);
CHECK(full(0) == -3.0);
CHECK(full(1) == 7.0);
CHECK(full(2) == -3.0);
CHECK(full(3) == -3.0);
CHECK(full(4) == 9.0);
CHECK(full(5) == -3.0);
}
TEST_CASE(
"Field DOF Map Scatter Add Accumulates Only Onto Active True DOFs",
tags::unit &tags::field
) {
namespace field = mean_field::field;
const field::FieldDofMap map(5, field_dof_map_test_utils::make_array({0, 2, 4}));
mfem::Vector reduced(3);
reduced(0) = 1.0;
reduced(1) = 2.0;
reduced(2) = 3.0;
mfem::Vector full(5);
full = 10.0;
map.scatter_add(reduced, full, 2.0);
CHECK(full(0) == 12.0);
CHECK(full(1) == 10.0);
CHECK(full(2) == 14.0);
CHECK(full(3) == 10.0);
CHECK(full(4) == 16.0);
}
TEST_CASE(
"Field DOF Map Operations Support MFEM Vector Views Without Resizing",
tags::unit &tags::field
) {
namespace field = mean_field::field;
const field::FieldDofMap map(5, field_dof_map_test_utils::make_array({1, 3}));
mfem::Vector storage(9);
storage = -8.0;
/*
* View [2, 7) of the parent vector.
*/
mfem::Vector fullView(storage.GetData() + 2, 5);
mfem::Vector reduced(2);
reduced(0) = 4.0;
reduced(1) = 6.0;
map.scatter_into(reduced, fullView);
/*
* Storage outside the view must remain untouched.
*/
CHECK(storage(0) == -8.0);
CHECK(storage(1) == -8.0);
CHECK(storage(7) == -8.0);
CHECK(storage(8) == -8.0);
/*
* Within the view, only active true DOFs change.
*/
CHECK(storage(2) == -8.0);
CHECK(storage(3) == 4.0);
CHECK(storage(4) == -8.0);
CHECK(storage(5) == 6.0);
CHECK(storage(6) == -8.0);
}
TEST_CASE(
"Field DOF Map Operations Reject Incompatible Vector Sizes",
tags::unit &tags::field
) {
namespace field = mean_field::field;
const field::FieldDofMap map(5, field_dof_map_test_utils::make_array({1, 3}));
mfem::Vector correctFull(5);
mfem::Vector wrongFull(4);
mfem::Vector correctReduced(2);
mfem::Vector wrongReduced(3);
CHECK_THROWS_AS(map.gather(wrongFull), std::invalid_argument);
CHECK_THROWS_AS(map.gather(correctFull, wrongReduced), std::invalid_argument);
CHECK_THROWS_AS(map.scatter(wrongReduced), std::invalid_argument);
CHECK_THROWS_AS(map.scatter(correctReduced, wrongFull), std::invalid_argument);
CHECK_THROWS_AS(map.scatter_into(wrongReduced, correctFull), std::invalid_argument);
CHECK_THROWS_AS(map.scatter_add(correctReduced, wrongFull), std::invalid_argument);
}
TEST_CASE(
"Field DOF Map Identity Mapping Is An Exact Vector Identity",
tags::unit &tags::field
) {
namespace field = mean_field::field;
const field::FieldDofMap map(4, field_dof_map_test_utils::make_array({0, 1, 2, 3}));
REQUIRE(map.is_identity());
REQUIRE(map.inactive_size() == 0);
mfem::Vector full(4);
full(0) = 0.1;
full(1) = -0.2;
full(2) = 3.7;
full(3) = 8.1;
const mfem::Vector reduced = map.gather(full);
const mfem::Vector restored = map.scatter(reduced);
for (int index = 0; index < full.Size(); ++index) {
CHECK(reduced(index) == full(index));
CHECK(restored(index) == full(index));
}
}
TEST_CASE(
"Field DOF Map Validates Field DOF Support Consistency",
tags::unit &tags::field
) {
namespace field = mean_field::field;
field::FieldDofSupport support;
support.activeTrueDofMarker.SetSize(5);
support.activeTrueDofMarker = 0;
support.activeTrueDofMarker[1] = 1;
support.activeTrueDofMarker[3] = 1;
support.activeTrueDofs = field_dof_map_test_utils::make_array({1, 3});
const field::FieldDofMap validMap(support);
CHECK(validMap.full_size() == 5);
CHECK(validMap.reduced_size() == 2);
/*
* Make the marker disagree with the list.
*/
support.activeTrueDofMarker[3] = 0;
CHECK_THROWS_AS((field::FieldDofMap(support)), std::invalid_argument);
}
TEST_CASE(
"Field DOF Map Factory Is Available Only For Spatial Registered Fields",
tags::unit &tags::field
) {
namespace field = mean_field::field;
STATIC_REQUIRE(field_dof_map_test_utils::CanMakeFieldDofMap<field::Density>);
STATIC_REQUIRE(field_dof_map_test_utils::CanMakeFieldDofMap<field::Enthalpy>);
STATIC_REQUIRE(field_dof_map_test_utils::CanMakeFieldDofMap<field::Gravity>);
STATIC_REQUIRE(field_dof_map_test_utils::CanMakeFieldDofMap<field::Displacement>);
STATIC_REQUIRE_FALSE(field_dof_map_test_utils::CanMakeFieldDofMap<field::BarotropicConstant>);
CHECK(true);
}
TEST_CASE(
"Field DOF Map Factory Exactly Preserves Density Support",
tags::integration &tags::field
) {
namespace field = mean_field::field;
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);
REQUIRE(finiteElementSpace != nullptr);
const field::FieldDofSupport support =
field::resolve_field_dof_support<field::Density, field_dof_map_test_utils::Schema>(*finiteElementSpace);
const field::FieldDofMap map =
field::make_field_dof_map<field::Density, field_dof_map_test_utils::Schema>(*finiteElementSpace);
REQUIRE(map.full_size() == finiteElementSpace->GetTrueVSize());
REQUIRE(map.reduced_size() == support.activeTrueDofs.Size());
REQUIRE(map.full_size() == support.activeTrueDofMarker.Size());
for (int reducedDof = 0; reducedDof < map.reduced_size(); ++reducedDof) {
CAPTURE(reducedDof);
CHECK(map.true_dof(reducedDof) == support.activeTrueDofs[reducedDof]);
}
for (int trueDof = 0; trueDof < map.full_size(); ++trueDof) {
CAPTURE(trueDof);
CHECK(map.contains_true_dof(trueDof) == (support.activeTrueDofMarker[trueDof] != 0));
}
const long long globalFullSize = field_dof_map_test_utils::global_sum(map.full_size());
const long long globalReducedSize = field_dof_map_test_utils::global_sum(map.reduced_size());
/*
* L2 density has independent vacuum element DOFs, so removing vacuum
* support must genuinely reduce the global nonlinear block.
*/
CHECK(globalReducedSize > 0);
CHECK(globalReducedSize < globalFullSize);
}
TEST_CASE(
"Field DOF Map Factory Exactly Preserves H1 Enthalpy Support",
tags::integration &tags::field
) {
namespace field = mean_field::field;
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);
REQUIRE(finiteElementSpace != nullptr);
const field::FieldDofSupport support =
field::resolve_field_dof_support<field::Enthalpy, field_dof_map_test_utils::Schema>(*finiteElementSpace);
const field::FieldDofMap map =
field::make_field_dof_map<field::Enthalpy, field_dof_map_test_utils::Schema>(*finiteElementSpace);
REQUIRE(map.reduced_size() == support.activeTrueDofs.Size());
for (int reducedDof = 0; reducedDof < map.reduced_size(); ++reducedDof) {
CHECK(map.true_dof(reducedDof) == support.activeTrueDofs[reducedDof]);
}
/*
* The separate field_mfem support tests already establish that shared
* Stellar/Vacuum H1 trace DOFs are active. This test establishes that
* FieldDofMap preserves that active set exactly, rather than applying
* a second reduction or reinterpretation.
*/
for (int trueDof = 0; trueDof < map.full_size(); ++trueDof) {
CHECK(map.contains_true_dof(trueDof) == (support.activeTrueDofMarker[trueDof] != 0));
}
const long long globalFullSize = field_dof_map_test_utils::global_sum(map.full_size());
const long long globalReducedSize = field_dof_map_test_utils::global_sum(map.reduced_size());
CHECK(globalReducedSize > 0);
CHECK(globalReducedSize < globalFullSize);
}
TEST_CASE(
"Field DOF Map Factory Produces Identity Maps For All Supported Fields",
tags::integration &tags::field
) {
namespace field = mean_field::field;
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);
REQUIRE(finiteElementSpace != nullptr);
const field::FieldDofMap map =
field::make_field_dof_map<field::Displacement, field_dof_map_test_utils::Schema>(*finiteElementSpace);
CHECK(map.is_identity());
CHECK(map.full_size() == finiteElementSpace->GetTrueVSize());
CHECK(map.reduced_size() == finiteElementSpace->GetTrueVSize());
CHECK(map.inactive_size() == 0);
for (int trueDof = 0; trueDof < map.full_size(); ++trueDof) {
CHECK(map.true_dof(trueDof) == trueDof);
CHECK(map.contains_true_dof(trueDof));
}
}
TEST_CASE(
"Field DOF Map Factory Uses Schema Material Bindings Rather Than Numeric Conventions",
tags::integration &tags::field
) {
namespace field = mean_field::field;
mfem::Mesh serialMesh = field_dof_map_test_utils::make_split_mesh(17, 29);
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);
REQUIRE(finiteElementSpace != nullptr);
const field::FieldDofMap map =
field::make_field_dof_map<field::Density, field_dof_map_test_utils::AlternateSchema>(*finiteElementSpace);
const field::FieldDofSupport support =
field::resolve_field_dof_support<field::Density, field_dof_map_test_utils::AlternateSchema>(
*finiteElementSpace
);
CHECK(map.full_size() == support.activeTrueDofMarker.Size());
CHECK(map.reduced_size() == support.activeTrueDofs.Size());
for (int trueDof = 0; trueDof < map.full_size(); ++trueDof) {
CHECK(map.contains_true_dof(trueDof) == (support.activeTrueDofMarker[trueDof] != 0));
}
}
TEST_CASE(
"Field DOF Map Reduced Vectors Round Trip Through Real Field Support",
tags::integration &tags::field
) {
namespace field = mean_field::field;
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);
REQUIRE(finiteElementSpace != nullptr);
const field::FieldDofMap map =
field::make_field_dof_map<field::Enthalpy, field_dof_map_test_utils::Schema>(*finiteElementSpace);
mfem::Vector reduced(map.reduced_size());
for (int reducedDof = 0; reducedDof < reduced.Size(); ++reducedDof) {
reduced(reducedDof) = 0.125 + 0.031 * static_cast<double>(reducedDof + 1);
}
const mfem::Vector full = map.scatter(reduced);
const mfem::Vector recovered = map.gather(full);
REQUIRE(recovered.Size() == reduced.Size());
for (int reducedDof = 0; reducedDof < reduced.Size(); ++reducedDof) {
CAPTURE(reducedDof);
CHECK(recovered(reducedDof) == reduced(reducedDof));
}
for (int trueDof = 0; trueDof < full.Size(); ++trueDof) {
if (!map.contains_true_dof(trueDof)) {
CHECK(full(trueDof) == 0.0);
}
}
}

778
tests/field/field_mfem.cpp Normal file
View File

@@ -0,0 +1,778 @@
#include <catch2/catch_test_macros.hpp>
#include <algorithm>
#include <array>
#include <concepts>
#include <cstddef>
#include <memory>
#include <mpi.h>
#include <set>
#include <stdexcept>
#include <string_view>
#include <vector>
#include <mfem.hpp>
import mean_field;
import test_helpers;
namespace field_mfem_test_utils {
namespace field = mean_field::field;
namespace domain = mean_field::utils::domain;
namespace quadrature = mean_field::quadrature;
using Schema = domain::CoreEnvelopeVacuumDomainSchema;
struct VectorL2Field {
static constexpr std::string_view name = "test_vector_l2";
using Support = field::DomainSupport<domain::All>;
struct Vector final : field::VectorQ<field::FieldRelation::Independent, field::Disc<field::L2, 2>> { };
using Quantities = field::TypeList<Vector>;
using Constraints = field::TypeList<>;
using FormList = field::TypeList<>;
static constexpr bool constraintsAreValid = field::validate_constraints(Constraints{});
static_assert(constraintsAreValid);
};
struct NdField {
static constexpr std::string_view name = "test_nd";
using Support = field::DomainSupport<domain::All>;
struct Vector final : field::VectorQ<field::FieldRelation::Independent, field::Disc<field::ND, 2>> { };
using Quantities = field::TypeList<Vector>;
using Constraints = field::TypeList<>;
using FormList = field::TypeList<>;
static constexpr bool constraintsAreValid = field::validate_constraints(Constraints{});
static_assert(constraintsAreValid);
};
using AlternateSchema = domain::DomainSchema<
domain::MaterialList<
domain::Material<domain::Core, 11>,
domain::Material<domain::Envelope, 17>,
domain::Material<domain::Vacuum, 29>>,
domain::BoundaryList<>,
domain::RelationList<>>;
template <typename FieldT>
concept CanResolveLocalSupport = requires(const mfem::FiniteElementSpace &space) {
field::resolve_field_local_dof_support<FieldT, Schema>(space);
};
[[nodiscard]]
mfem::Mesh make_two_domain_mesh(
const int leftAttribute = 2,
const int rightAttribute = 3
) {
mfem::Mesh mesh = mfem::Mesh::MakeCartesian2D(2, 1, mfem::Element::QUADRILATERAL, true, 2.0, 1.0);
mesh.GetElement(0)->SetAttribute(leftAttribute);
mesh.GetElement(1)->SetAttribute(rightAttribute);
mesh.SetAttributes();
return mesh;
}
[[nodiscard]]
mfem::Mesh make_parallel_split_mesh() {
constexpr int xElementCount = 4;
constexpr int yElementCount = 2;
mfem::Mesh mesh =
mfem::Mesh::MakeCartesian2D(xElementCount, yElementCount, mfem::Element::QUADRILATERAL, true, 4.0, 2.0);
for (int elementId = 0; elementId < mesh.GetNE(); ++elementId) {
const int xIndex = elementId % xElementCount;
const int attribute = xIndex < 2 ? 2 : 3;
mesh.GetElement(elementId)->SetAttribute(attribute);
}
mesh.SetAttributes();
return mesh;
}
[[nodiscard]]
std::vector<int> decoded_element_vdofs(
const mfem::FiniteElementSpace &space,
const int elementId
) {
mfem::Array<int> signedVDofs;
space.GetElementVDofs(elementId, signedVDofs);
std::vector<int> result;
result.reserve(static_cast<std::size_t>(signedVDofs.Size()));
for (int index = 0; index < signedVDofs.Size(); ++index) {
result.push_back(mfem::FiniteElementSpace::DecodeDof(signedVDofs[index]));
}
std::ranges::sort(result);
result.erase(std::unique(result.begin(), result.end()), result.end());
return result;
}
[[nodiscard]]
bool contains(
const mfem::Array<int> &values,
const int value
) {
for (int index = 0; index < values.Size(); ++index) {
if (values[index] == value) {
return true;
}
}
return false;
}
[[nodiscard]]
std::vector<int> intersection(
const std::vector<int> &first,
const std::vector<int> &second
) {
std::vector<int> result;
std::set_intersection(first.begin(), first.end(), second.begin(), second.end(), std::back_inserter(result));
return result;
}
[[nodiscard]]
std::vector<int> difference(
const std::vector<int> &first,
const std::vector<int> &second
) {
std::vector<int> result;
std::set_difference(first.begin(), first.end(), second.begin(), second.end(), std::back_inserter(result));
return result;
}
[[nodiscard]]
long long global_sum(const int localValue) {
const long long local = static_cast<long long>(localValue);
long long global = 0;
MPI_Allreduce(&local, &global, 1, MPI_LONG_LONG, MPI_SUM, MPI_COMM_WORLD);
return global;
}
} // namespace field_mfem_test_utils
TEST_CASE(
"Field MFEM Support Resolution Is Available Only For Domain Supported Fields",
tags::unit &tags::field
) {
namespace field = mean_field::field;
STATIC_REQUIRE(field::MfemDomainField<field::Density>);
STATIC_REQUIRE(field::MfemDomainField<field::Enthalpy>);
STATIC_REQUIRE(field::MfemDomainField<field::Gravity>);
STATIC_REQUIRE(field::MfemDomainField<field::Displacement>);
STATIC_REQUIRE_FALSE(field::MfemDomainField<field::BarotropicConstant>);
STATIC_REQUIRE(field_mfem_test_utils::CanResolveLocalSupport<field::Density>);
STATIC_REQUIRE_FALSE(field_mfem_test_utils::CanResolveLocalSupport<field::BarotropicConstant>);
CHECK(true);
}
TEST_CASE(
"Field MFEM Creates The Registered Finite Element Collection Families",
tags::unit &tags::field
) {
namespace field = mean_field::field;
using DensityField = field::Field<field::Density>;
using GravityField = field::Field<field::Gravity>;
using DisplacementField = field::Field<field::Displacement>;
using EnthalpyField = field::Field<field::Enthalpy>;
auto densityCollection = DensityField::make_fec<field::Density::Scalar>(3);
auto potentialCollection = GravityField::make_fec<field::Gravity::Potential>(3);
auto fluxCollection = GravityField::make_fec<field::Gravity::Flux>(3);
auto displacementCollection = DisplacementField::make_fec<field::Displacement::Vector>(3);
auto enthalpyCollection = EnthalpyField::make_fec<field::Enthalpy::Scalar>(3);
auto vectorL2Collection =
field::Field<field_mfem_test_utils::VectorL2Field>::make_fec<field_mfem_test_utils::VectorL2Field::Vector>(3);
auto ndCollection =
field::Field<field_mfem_test_utils::NdField>::make_fec<field_mfem_test_utils::NdField::Vector>(3);
REQUIRE(densityCollection != nullptr);
REQUIRE(potentialCollection != nullptr);
REQUIRE(fluxCollection != nullptr);
REQUIRE(displacementCollection != nullptr);
REQUIRE(enthalpyCollection != nullptr);
REQUIRE(vectorL2Collection != nullptr);
REQUIRE(ndCollection != nullptr);
CHECK(dynamic_cast<mfem::L2_FECollection *>(densityCollection.get()) != nullptr);
CHECK(dynamic_cast<mfem::L2_FECollection *>(potentialCollection.get()) != nullptr);
CHECK(dynamic_cast<mfem::RT_FECollection *>(fluxCollection.get()) != nullptr);
CHECK(dynamic_cast<mfem::H1_FECollection *>(displacementCollection.get()) != nullptr);
CHECK(dynamic_cast<mfem::H1_FECollection *>(enthalpyCollection.get()) != nullptr);
CHECK(dynamic_cast<mfem::L2_FECollection *>(vectorL2Collection.get()) != nullptr);
CHECK(dynamic_cast<mfem::ND_FECollection *>(ndCollection.get()) != nullptr);
CHECK_THROWS_AS((DensityField::make_fec<field::Density::Scalar>(0)), std::invalid_argument);
CHECK_THROWS_AS((GravityField::make_fec<field::Gravity::Flux>(-1)), std::invalid_argument);
}
TEST_CASE(
"Field MFEM Creates Parallel Spaces With Registered Dimensions Orders And Ordering",
tags::integration &tags::field
) {
namespace field = mean_field::field;
mfem::Mesh serialMesh = mfem::Mesh::MakeCartesian2D(4, 2, mfem::Element::QUADRILATERAL, true, 4.0, 2.0);
mfem::ParMesh mesh(MPI_COMM_WORLD, serialMesh);
auto densityFec = field::Field<field::Density>::make_fec<field::Density::Scalar>(2);
auto potentialFec = field::Field<field::Gravity>::make_fec<field::Gravity::Potential>(2);
auto fluxFec = field::Field<field::Gravity>::make_fec<field::Gravity::Flux>(2);
auto displacementFec = field::Field<field::Displacement>::make_fec<field::Displacement::Vector>(2);
auto enthalpyFec = field::Field<field::Enthalpy>::make_fec<field::Enthalpy::Scalar>(2);
auto vectorL2Fec =
field::Field<field_mfem_test_utils::VectorL2Field>::make_fec<field_mfem_test_utils::VectorL2Field::Vector>(2);
auto ndFec = field::Field<field_mfem_test_utils::NdField>::make_fec<field_mfem_test_utils::NdField::Vector>(2);
auto densitySpace = field::Field<field::Density>::make_fespace<field::Density::Scalar>(mesh, *densityFec);
auto potentialSpace = field::Field<field::Gravity>::make_fespace<field::Gravity::Potential>(mesh, *potentialFec);
auto fluxSpace = field::Field<field::Gravity>::make_fespace<field::Gravity::Flux>(mesh, *fluxFec);
auto displacementSpace =
field::Field<field::Displacement>::make_fespace<field::Displacement::Vector>(mesh, *displacementFec);
auto enthalpySpace = field::Field<field::Enthalpy>::make_fespace<field::Enthalpy::Scalar>(mesh, *enthalpyFec);
auto vectorL2Space =
field::Field<field_mfem_test_utils::VectorL2Field>::make_fespace<field_mfem_test_utils::VectorL2Field::Vector>(
mesh, *vectorL2Fec
);
auto ndSpace = field::Field<field_mfem_test_utils::NdField>::make_fespace<field_mfem_test_utils::NdField::Vector>(
mesh, *ndFec
);
REQUIRE(densitySpace != nullptr);
REQUIRE(potentialSpace != nullptr);
REQUIRE(fluxSpace != nullptr);
REQUIRE(displacementSpace != nullptr);
REQUIRE(enthalpySpace != nullptr);
REQUIRE(vectorL2Space != nullptr);
REQUIRE(ndSpace != nullptr);
CHECK(densitySpace->GetVDim() == 1);
CHECK(potentialSpace->GetVDim() == 1);
CHECK(fluxSpace->GetVDim() == 1);
CHECK(enthalpySpace->GetVDim() == 1);
CHECK(displacementSpace->GetVDim() == mesh.SpaceDimension());
CHECK(vectorL2Space->GetVDim() == mesh.SpaceDimension());
CHECK(ndSpace->GetVDim() == 1);
CHECK(densitySpace->GetOrdering() == mfem::Ordering::byNODES);
CHECK(potentialSpace->GetOrdering() == mfem::Ordering::byNODES);
CHECK(fluxSpace->GetOrdering() == mfem::Ordering::byNODES);
CHECK(enthalpySpace->GetOrdering() == mfem::Ordering::byNODES);
/*
* Displacement deliberately overrides the generic
* vector-H1 rule and is part of the project's block/indexing
* contract.
*/
CHECK(displacementSpace->GetOrdering() == mfem::Ordering::byNODES);
/*
* A generic vector L2 quantity retains the ordinary backend
* realization, demonstrating that the displacement behavior is
* an intentional specialization rather than a global accident.
*/
CHECK(vectorL2Space->GetOrdering() == mfem::Ordering::byVDIM);
CHECK(ndSpace->GetOrdering() == mfem::Ordering::byNODES);
CHECK(densitySpace->GetMaxElementOrder() == field::Density::Scalar::familyOrder);
CHECK(potentialSpace->GetMaxElementOrder() == field::Gravity::Potential::familyOrder);
CHECK(fluxSpace->GetMaxElementOrder() == field::Gravity::Flux::familyOrder + 1);
CHECK(displacementSpace->GetMaxElementOrder() == field::Displacement::Vector::familyOrder);
CHECK(enthalpySpace->GetMaxElementOrder() == field::Enthalpy::Scalar::familyOrder);
}
TEST_CASE(
"Field MFEM Typed Queries Preserve Backend Polynomial Order Semantics",
tags::unit &tags::field
) {
namespace field = mean_field::field;
namespace quadrature = mean_field::quadrature;
namespace utils = mean_field::utils;
using DensityField = field::Field<field::Density>;
using GravityField = field::Field<field::Gravity>;
using EnthalpyField = field::Field<field::Enthalpy>;
const quadrature::Query densitySource = DensityField::make_query<field::Density::Form::ProjectionSource>(
quadrature::QuadratureRole::projection, 3, std::array<int, 1>{4}, utils::DOMAINS::STELLAR,
quadrature::MappingKind::general
);
REQUIRE(densitySource.base_order.has_value());
/*
* L2_2 value order 2
* + geometry order 3
* + dynamic coefficient order 4.
*/
CHECK(*densitySource.base_order == 9);
CHECK(densitySource.term == quadrature::Term::density_projection);
CHECK(densitySource.role == quadrature::QuadratureRole::projection);
CHECK(densitySource.domain == utils::DOMAINS::STELLAR);
CHECK(densitySource.mapping == quadrature::MappingKind::general);
CHECK(densitySource.geometry_weight_order == 3);
const quadrature::Query hdivMass =
GravityField::make_query<field::Gravity::Form::HDivMass>(quadrature::QuadratureRole::discretization, 2);
REQUIRE(hdivMass.base_order.has_value());
/*
* RT_2 value order is 3, hence
* 3 + 3 + geometry 2 = 8.
*/
CHECK(*hdivMass.base_order == 8);
const quadrature::Query divergence = GravityField::make_query<field::Gravity::Form::DivergenceCoupling>(
quadrature::QuadratureRole::discretization, 2
);
REQUIRE(divergence.base_order.has_value());
/*
* div(RT_2) order 2
* + L2_2 order 2
* + geometry 2.
*/
CHECK(*divergence.base_order == 6);
const quadrature::Query pressureForce = EnthalpyField::make_query<field::Enthalpy::Form::PressureForce>(
quadrature::QuadratureRole::discretization, 2, std::array<int, 1>{9}, utils::DOMAINS::STELLAR,
quadrature::MappingKind::general
);
REQUIRE(pressureForce.base_order.has_value());
/*
* h value order 3
* + grad(d) order 2
* + geometry 2
* + n=3 pressure extra order 9
* = 16.
*/
CHECK(*pressureForce.base_order == 16);
const quadrature::Query equilibriumConstant = EnthalpyField::make_query<field::Enthalpy::Form::EquilibriumConstant>(
quadrature::QuadratureRole::discretization, 2
);
REQUIRE(equilibriumConstant.base_order.has_value());
/*
* Global scalar C contributes zero polynomial order,
* h contributes 3, and geometry contributes 2.
*/
CHECK(*equilibriumConstant.base_order == 5);
CHECK_THROWS_AS(
(DensityField::make_query<field::Density::Form::ProjectionMass>(quadrature::QuadratureRole::projection, -1)),
std::invalid_argument
);
const std::array<int, 1> negativeDynamicOrder{-1};
CHECK_THROWS_AS(
(EnthalpyField::make_query<field::Enthalpy::Form::PressureForce>(
quadrature::QuadratureRole::discretization, 2, negativeDynamicOrder
)),
std::invalid_argument
);
}
TEST_CASE(
"Field MFEM Element Support Resolves Semantic Domains Through The Schema",
tags::unit &tags::field
) {
namespace field = mean_field::field;
const mfem::Mesh mesh = field_mfem_test_utils::make_two_domain_mesh();
CHECK((field::element_is_in_field_support<field::Density, field_mfem_test_utils::Schema>(mesh, 0)));
CHECK_FALSE((field::element_is_in_field_support<field::Density, field_mfem_test_utils::Schema>(mesh, 1)));
CHECK((field::element_is_in_field_support<field::Enthalpy, field_mfem_test_utils::Schema>(mesh, 0)));
CHECK_FALSE((field::element_is_in_field_support<field::Enthalpy, field_mfem_test_utils::Schema>(mesh, 1)));
CHECK((field::element_is_in_field_support<field::Gravity, field_mfem_test_utils::Schema>(mesh, 0)));
CHECK((field::element_is_in_field_support<field::Gravity, field_mfem_test_utils::Schema>(mesh, 1)));
CHECK((field::element_is_in_field_support<field::Displacement, field_mfem_test_utils::Schema>(mesh, 0)));
CHECK((field::element_is_in_field_support<field::Displacement, field_mfem_test_utils::Schema>(mesh, 1)));
}
TEST_CASE(
"Field MFEM L2 Stellar Support Selects Exactly Stellar Element DOFs",
tags::unit &tags::field
) {
namespace field = mean_field::field;
mfem::Mesh mesh = field_mfem_test_utils::make_two_domain_mesh();
auto fec = field::Field<field::Density>::make_fec<field::Density::Scalar>(2);
mfem::FiniteElementSpace space(&mesh, fec.get());
const auto support = field::resolve_field_local_dof_support<field::Density, field_mfem_test_utils::Schema>(space);
const std::vector<int> stellarVDofs = field_mfem_test_utils::decoded_element_vdofs(space, 0);
const std::vector<int> vacuumVDofs = field_mfem_test_utils::decoded_element_vdofs(space, 1);
REQUIRE_FALSE(stellarVDofs.empty());
REQUIRE_FALSE(vacuumVDofs.empty());
CHECK(support.activeVDofMarker.Size() == space.GetVSize());
CHECK(support.activeVDofs.Size() + support.inactiveVDofs.Size() == space.GetVSize());
for (const int vdof : stellarVDofs) {
CAPTURE(vdof);
CHECK(support.activeVDofMarker[vdof] == 1);
CHECK(field_mfem_test_utils::contains(support.activeVDofs, vdof));
CHECK_FALSE(field_mfem_test_utils::contains(support.inactiveVDofs, vdof));
}
for (const int vdof : vacuumVDofs) {
CAPTURE(vdof);
CHECK(support.activeVDofMarker[vdof] == 0);
CHECK_FALSE(field_mfem_test_utils::contains(support.activeVDofs, vdof));
CHECK(field_mfem_test_utils::contains(support.inactiveVDofs, vdof));
}
CHECK(support.activeVDofs.Size() == static_cast<int>(stellarVDofs.size()));
CHECK(support.inactiveVDofs.Size() == static_cast<int>(vacuumVDofs.size()));
}
TEST_CASE(
"Field MFEM H1 Stellar Support Keeps Shared Stellar Vacuum Trace DOFs Active",
tags::unit &tags::field
) {
namespace field = mean_field::field;
mfem::Mesh mesh = field_mfem_test_utils::make_two_domain_mesh();
auto fec = field::Field<field::Enthalpy>::make_fec<field::Enthalpy::Scalar>(2);
mfem::FiniteElementSpace space(&mesh, fec.get());
const auto support = field::resolve_field_local_dof_support<field::Enthalpy, field_mfem_test_utils::Schema>(space);
const std::vector<int> stellarVDofs = field_mfem_test_utils::decoded_element_vdofs(space, 0);
const std::vector<int> vacuumVDofs = field_mfem_test_utils::decoded_element_vdofs(space, 1);
const std::vector<int> interfaceVDofs = field_mfem_test_utils::intersection(stellarVDofs, vacuumVDofs);
const std::vector<int> vacuumOnlyVDofs = field_mfem_test_utils::difference(vacuumVDofs, stellarVDofs);
REQUIRE_FALSE(interfaceVDofs.empty());
REQUIRE_FALSE(vacuumOnlyVDofs.empty());
for (const int vdof : stellarVDofs) {
CAPTURE(vdof);
CHECK(support.activeVDofMarker[vdof] == 1);
}
/*
* This is the central support invariant:
*
* shared interface DOFs are active because they are touched
* by a supported stellar element, even though they are also
* touched by a vacuum element.
*/
for (const int vdof : interfaceVDofs) {
CAPTURE(vdof);
CHECK(support.activeVDofMarker[vdof] == 1);
CHECK(field_mfem_test_utils::contains(support.activeVDofs, vdof));
}
for (const int vdof : vacuumOnlyVDofs) {
CAPTURE(vdof);
CHECK(support.activeVDofMarker[vdof] == 0);
CHECK(field_mfem_test_utils::contains(support.inactiveVDofs, vdof));
}
CHECK(support.activeVDofs.Size() + support.inactiveVDofs.Size() == space.GetVSize());
}
TEST_CASE(
"Field MFEM All Domain Support Activates Every L2 And RT DOF",
tags::unit &tags::field
) {
namespace field = mean_field::field;
mfem::Mesh mesh = field_mfem_test_utils::make_two_domain_mesh();
auto potentialFec = field::Field<field::Gravity>::make_fec<field::Gravity::Potential>(2);
mfem::FiniteElementSpace potentialSpace(&mesh, potentialFec.get());
const auto potentialSupport =
field::resolve_field_local_dof_support<field::Gravity, field_mfem_test_utils::Schema>(potentialSpace);
CHECK(potentialSupport.activeVDofs.Size() == potentialSpace.GetVSize());
CHECK(potentialSupport.inactiveVDofs.Size() == 0);
for (int vdof = 0; vdof < potentialSupport.activeVDofMarker.Size(); ++vdof) {
CHECK(potentialSupport.activeVDofMarker[vdof] == 1);
}
/*
* Exercise signed/oriented MFEM element VDofs through RT as
* well. The support resolver must DecodeDof() correctly.
*/
auto fluxFec = field::Field<field::Gravity>::make_fec<field::Gravity::Flux>(2);
mfem::FiniteElementSpace fluxSpace(&mesh, fluxFec.get());
const auto fluxSupport =
field::resolve_field_local_dof_support<field::Gravity, field_mfem_test_utils::Schema>(fluxSpace);
CHECK(fluxSupport.activeVDofs.Size() == fluxSpace.GetVSize());
CHECK(fluxSupport.inactiveVDofs.Size() == 0);
for (int vdof = 0; vdof < fluxSupport.activeVDofMarker.Size(); ++vdof) {
CHECK(fluxSupport.activeVDofMarker[vdof] == 1);
}
}
TEST_CASE(
"Field MFEM Support Resolution Uses Schema Material Bindings Rather Than Hard Coded IDs",
tags::unit &tags::field
) {
namespace field = mean_field::field;
mfem::Mesh mesh = field_mfem_test_utils::make_two_domain_mesh(17, 29);
auto fec = field::Field<field::Density>::make_fec<field::Density::Scalar>(2);
mfem::FiniteElementSpace space(&mesh, fec.get());
const auto support =
field::resolve_field_local_dof_support<field::Density, field_mfem_test_utils::AlternateSchema>(space);
const std::vector<int> stellarVDofs = field_mfem_test_utils::decoded_element_vdofs(space, 0);
const std::vector<int> vacuumVDofs = field_mfem_test_utils::decoded_element_vdofs(space, 1);
for (const int vdof : stellarVDofs) {
CHECK(support.activeVDofMarker[vdof] == 1);
}
for (const int vdof : vacuumVDofs) {
CHECK(support.activeVDofMarker[vdof] == 0);
}
}
TEST_CASE(
"Field MFEM Parallel Stellar Support Produces Consistent Local And True DOF Partitions",
tags::integration &tags::field
) {
namespace field = mean_field::field;
mfem::Mesh serialMesh = field_mfem_test_utils::make_parallel_split_mesh();
mfem::ParMesh mesh(MPI_COMM_WORLD, serialMesh);
auto fec = field::Field<field::Enthalpy>::make_fec<field::Enthalpy::Scalar>(2);
auto space = field::Field<field::Enthalpy>::make_fespace<field::Enthalpy::Scalar>(mesh, *fec);
REQUIRE(space != nullptr);
const auto support = field::resolve_field_dof_support<field::Enthalpy, field_mfem_test_utils::Schema>(*space);
CHECK(support.activeVDofMarker.Size() == space->GetVSize());
CHECK(support.activeVDofs.Size() + support.inactiveVDofs.Size() == space->GetVSize());
CHECK(support.activeTrueDofMarker.Size() == space->GetTrueVSize());
CHECK(support.activeTrueDofs.Size() + support.inactiveTrueDofs.Size() == space->GetTrueVSize());
/*
* Every local DOF touched by a supported element must be active
* after shared-DOF synchronization.
*/
for (int elementId = 0; elementId < mesh.GetNE(); ++elementId) {
const int materialId = mesh.GetAttribute(elementId);
const bool stellar =
field_mfem_test_utils::Schema::template attribute_belongs_to<mean_field::utils::domain::Stellar>(
materialId
);
if (!stellar) {
continue;
}
const std::vector<int> vdofs = field_mfem_test_utils::decoded_element_vdofs(*space, elementId);
for (const int vdof : vdofs) {
CAPTURE(elementId, vdof);
CHECK(support.activeVDofMarker[vdof] == 1);
}
}
const long long globalActiveTrueDofs = field_mfem_test_utils::global_sum(support.activeTrueDofs.Size());
const long long globalInactiveTrueDofs = field_mfem_test_utils::global_sum(support.inactiveTrueDofs.Size());
/*
* The split mesh contains a finite stellar region and a finite
* vacuum region with order-three H1 structure, so both categories
* must genuinely exist globally.
*/
CHECK(globalActiveTrueDofs > 0);
CHECK(globalInactiveTrueDofs > 0);
}
TEST_CASE(
"Field MFEM Parallel All Support Activates Every True Displacement DOF",
tags::integration &tags::field
) {
namespace field = mean_field::field;
mfem::Mesh serialMesh = field_mfem_test_utils::make_parallel_split_mesh();
mfem::ParMesh mesh(MPI_COMM_WORLD, serialMesh);
auto fec = field::Field<field::Displacement>::make_fec<field::Displacement::Vector>(2);
auto space = field::Field<field::Displacement>::make_fespace<field::Displacement::Vector>(mesh, *fec);
REQUIRE(space != nullptr);
const auto support = field::resolve_field_dof_support<field::Displacement, field_mfem_test_utils::Schema>(*space);
CHECK(support.inactiveVDofs.Size() == 0);
CHECK(support.activeVDofs.Size() == space->GetVSize());
CHECK(support.inactiveTrueDofs.Size() == 0);
CHECK(support.activeTrueDofs.Size() == space->GetTrueVSize());
for (int index = 0; index < support.activeTrueDofMarker.Size(); ++index) {
CHECK(support.activeTrueDofMarker[index] == 1);
}
}

View File

@@ -0,0 +1,427 @@
#include <catch2/catch_test_macros.hpp>
#include <concepts>
#include <cstddef>
#include <string_view>
#include <type_traits>
import mean_field;
import test_helpers;
namespace field_registry_test_utils {
namespace field = mean_field::field;
namespace domain = mean_field::utils::domain;
namespace quadrature = mean_field::quadrature;
template <typename ListT> struct TypeListSize;
template <typename... Ts>
struct TypeListSize<field::TypeList<Ts...>> : std::integral_constant<std::size_t, sizeof...(Ts)> { };
template <typename ListT> inline constexpr std::size_t typeListSize = TypeListSize<ListT>::value;
struct MissingSupportField {
static constexpr std::string_view name = "missing_support";
using Quantities = field::TypeList<field::GlobalScalarQ>;
using Constraints = field::TypeList<>;
using FormList = field::TypeList<>;
};
struct InvalidSupportField {
static constexpr std::string_view name = "invalid_support";
struct InvalidSupport { };
using Support = InvalidSupport;
using Quantities = field::TypeList<field::GlobalScalarQ>;
using Constraints = field::TypeList<>;
using FormList = field::TypeList<>;
};
struct InvalidQuantityListField {
static constexpr std::string_view name = "invalid_quantity_list";
using Support = field::NonSpatialSupport;
using Quantities = field::TypeList<int>;
using Constraints = field::TypeList<>;
using FormList = field::TypeList<>;
};
struct InvalidFormListField {
static constexpr std::string_view name = "invalid_form_list";
using Support = field::NonSpatialSupport;
using Quantities = field::TypeList<field::GlobalScalarQ>;
using Constraints = field::TypeList<>;
using FormList = field::TypeList<int>;
};
} // namespace field_registry_test_utils
TEST_CASE(
"Field Registry Recognizes Every Production Field And Rejects Incomplete Definitions",
tags::unit &tags::field
) {
namespace field = mean_field::field;
STATIC_REQUIRE(field::FieldTag<field::Density>);
STATIC_REQUIRE(field::FieldTag<field::Gravity>);
STATIC_REQUIRE(field::FieldTag<field::Displacement>);
STATIC_REQUIRE(field::FieldTag<field::Enthalpy>);
STATIC_REQUIRE(field::FieldTag<field::BarotropicConstant>);
STATIC_REQUIRE_FALSE(field::FieldTag<field_registry_test_utils::MissingSupportField>);
STATIC_REQUIRE_FALSE(field::FieldTag<field_registry_test_utils::InvalidSupportField>);
STATIC_REQUIRE_FALSE(field::FieldTag<field_registry_test_utils::InvalidQuantityListField>);
STATIC_REQUIRE_FALSE(field::FieldTag<field_registry_test_utils::InvalidFormListField>);
CHECK(true);
}
TEST_CASE(
"Field Registry Assigns The Intended Semantic Support To Every Production Field",
tags::unit &tags::field
) {
namespace field = mean_field::field;
namespace domain = mean_field::utils::domain;
STATIC_REQUIRE(field::DomainSupportedField<field::Density>);
STATIC_REQUIRE(field::DomainSupportedField<field::Enthalpy>);
STATIC_REQUIRE(field::DomainSupportedField<field::Gravity>);
STATIC_REQUIRE(field::DomainSupportedField<field::Displacement>);
STATIC_REQUIRE(field::NonSpatialField<field::BarotropicConstant>);
STATIC_REQUIRE(std::same_as<field::FieldDomainT<field::Density>, domain::Stellar>);
STATIC_REQUIRE(std::same_as<field::FieldDomainT<field::Enthalpy>, domain::Stellar>);
STATIC_REQUIRE(std::same_as<field::FieldDomainT<field::Gravity>, domain::All>);
STATIC_REQUIRE(std::same_as<field::FieldDomainT<field::Displacement>, domain::All>);
STATIC_REQUIRE(std::same_as<field::FieldSupportT<field::BarotropicConstant>, field::NonSpatialSupport>);
CHECK(true);
}
TEST_CASE(
"Density Registry Definition Is Complete And Self Consistent",
tags::unit &tags::field
) {
namespace field = mean_field::field;
namespace quadrature = mean_field::quadrature;
CHECK(field::Density::name == std::string_view{"density"});
CHECK(field::Density::Scalar::symbol == std::string_view{"ρ"});
STATIC_REQUIRE(field::Density::scalarOrder == 2);
STATIC_REQUIRE(field::Density::Scalar::rankValue == 0);
STATIC_REQUIRE(field::Density::Scalar::familyOrder == field::Density::scalarOrder);
STATIC_REQUIRE(std::same_as<typename field::Density::Scalar::Space, field::L2>);
STATIC_REQUIRE(std::same_as<typename field::Density::Scalar::Relation, field::FieldRelation::Independent>);
STATIC_REQUIRE(field_registry_test_utils::typeListSize<field::Density::Quantities> == 1);
STATIC_REQUIRE(field_registry_test_utils::typeListSize<field::Density::Constraints> == 0);
STATIC_REQUIRE(field_registry_test_utils::typeListSize<field::Density::FormList> == 8);
STATIC_REQUIRE(field::Density::constraintsAreValid);
STATIC_REQUIRE(field::typeListContains<field::Density::Form::ProjectionMass, field::Density::FormList>);
STATIC_REQUIRE(field::typeListContains<field::Density::Form::ProjectionSource, field::Density::FormList>);
STATIC_REQUIRE(field::typeListContains<field::Density::Form::EosClosureMass, field::Density::FormList>);
STATIC_REQUIRE(field::typeListContains<field::Density::Form::MassConservation, field::Density::FormList>);
STATIC_REQUIRE(field::typeListContains<field::Density::Form::MassNormalization, field::Density::FormList>);
STATIC_REQUIRE(field::typeListContains<field::Density::Form::CenterOfMass, field::Density::FormList>);
STATIC_REQUIRE(field::typeListContains<field::Density::Form::Quadrupole, field::Density::FormList>);
STATIC_REQUIRE(field::typeListContains<field::Density::Form::ErrorNorm, field::Density::FormList>);
STATIC_REQUIRE(field::Density::Form::ProjectionMass::policyKey == quadrature::Term::density_projection);
STATIC_REQUIRE(field::Density::Form::ProjectionMass::dynamicOrderCount == 0);
STATIC_REQUIRE(
std::same_as<
typename field::Density::Form::ProjectionMass::Operands,
field::TypeList<field::Operand<field::Density::Scalar>, field::Operand<field::Density::Scalar>>>
);
STATIC_REQUIRE(field::Density::Form::ProjectionSource::dynamicOrderCount == 1);
STATIC_REQUIRE(field::Density::Form::EosClosureMass::policyKey == quadrature::Term::eos_closure);
STATIC_REQUIRE(field::Density::Form::MassConservation::policyKey == quadrature::Term::mass_conservation);
STATIC_REQUIRE(field::Density::Form::MassNormalization::policyKey == quadrature::Term::mass_normalization);
STATIC_REQUIRE(field::Density::Form::CenterOfMass::dynamicOrderCount == 1);
STATIC_REQUIRE(field::Density::Form::Quadrupole::dynamicOrderCount == 1);
CHECK(true);
}
TEST_CASE(
"Gravity Registry Defines A Stable Mixed RT L2 Pair And All Registered Forms",
tags::unit &tags::field
) {
namespace field = mean_field::field;
namespace quadrature = mean_field::quadrature;
CHECK(field::Gravity::name == std::string_view{"gravity"});
CHECK(field::Gravity::Potential::symbol == std::string_view{"φ"});
CHECK(field::Gravity::Flux::symbol == std::string_view{"∇φ"});
STATIC_REQUIRE(field::Gravity::potentialOrder == 2);
STATIC_REQUIRE(field::Gravity::fluxOrder == 2);
STATIC_REQUIRE(std::same_as<typename field::Gravity::Potential::Space, field::L2>);
STATIC_REQUIRE(std::same_as<typename field::Gravity::Flux::Space, field::RT>);
STATIC_REQUIRE(field::Gravity::Potential::rankValue == 0);
STATIC_REQUIRE(field::Gravity::Flux::rankValue == 1);
STATIC_REQUIRE(field::DerivedQuantity<field::Gravity::Flux>);
STATIC_REQUIRE(std::same_as<field::RelationTargetT<field::Gravity::Flux>, field::Gravity::Potential>);
STATIC_REQUIRE(field_registry_test_utils::typeListSize<field::Gravity::Quantities> == 2);
STATIC_REQUIRE(field_registry_test_utils::typeListSize<field::Gravity::Constraints> == 1);
STATIC_REQUIRE(field_registry_test_utils::typeListSize<field::Gravity::FormList> == 7);
STATIC_REQUIRE(field::Gravity::constraintsAreValid);
STATIC_REQUIRE(field::typeListContains<field::Gravity::Form::HDivMass, field::Gravity::FormList>);
STATIC_REQUIRE(field::typeListContains<field::Gravity::Form::DivergenceCoupling, field::Gravity::FormList>);
STATIC_REQUIRE(field::typeListContains<field::Gravity::Form::Boundary, field::Gravity::FormList>);
STATIC_REQUIRE(field::typeListContains<field::Gravity::Form::SourceLinear, field::Gravity::FormList>);
STATIC_REQUIRE(field::typeListContains<field::Gravity::Form::SourceProjection, field::Gravity::FormList>);
STATIC_REQUIRE(field::typeListContains<field::Gravity::Form::PotentialErrorNorm, field::Gravity::FormList>);
STATIC_REQUIRE(field::typeListContains<field::Gravity::Form::FluxErrorNorm, field::Gravity::FormList>);
STATIC_REQUIRE(field::Gravity::Form::HDivMass::policyKey == quadrature::Term::gravity_hdiv_mass);
STATIC_REQUIRE(
std::same_as<
typename field::Gravity::Form::HDivMass::Operands,
field::TypeList<field::Operand<field::Gravity::Flux>, field::Operand<field::Gravity::Flux>>>
);
STATIC_REQUIRE(
std::same_as<
typename field::Gravity::Form::DivergenceCoupling::Operands,
field::TypeList<
field::Operand<field::Gravity::Flux, field::FieldOperation::Divergence>,
field::Operand<field::Gravity::Potential>>>
);
STATIC_REQUIRE(
std::same_as<
typename field::Gravity::Form::Boundary::Operands,
field::TypeList<
field::Operand<field::Gravity::Flux, field::FieldOperation::NormalTrace>,
field::Operand<field::Gravity::Flux, field::FieldOperation::NormalTrace>>>
);
STATIC_REQUIRE(
std::same_as<
typename field::Gravity::Form::SourceLinear::Operands,
field::TypeList<field::Operand<field::Density::Scalar>, field::Operand<field::Gravity::Potential>>>
);
CHECK(true);
}
TEST_CASE(
"Displacement Registry Preserves Vector H1 Geometry And Force Forms",
tags::unit &tags::field
) {
namespace field = mean_field::field;
namespace quadrature = mean_field::quadrature;
CHECK(field::Displacement::name == std::string_view{"displacement"});
CHECK(field::Displacement::Vector::symbol == std::string_view{"d"});
STATIC_REQUIRE(field::Displacement::vectorOrder == 3);
STATIC_REQUIRE(field::Displacement::Vector::rankValue == 1);
STATIC_REQUIRE(std::same_as<typename field::Displacement::Vector::Space, field::H1>);
STATIC_REQUIRE(field_registry_test_utils::typeListSize<field::Displacement::FormList> == 4);
STATIC_REQUIRE(field::Displacement::constraintsAreValid);
STATIC_REQUIRE(field::Displacement::Form::MeshExtension::policyKey == quadrature::Term::mesh_extension);
STATIC_REQUIRE(
std::same_as<
typename field::Displacement::Form::MeshExtension::Operands,
field::TypeList<
field::Operand<field::Displacement::Vector, field::FieldOperation::Gradient>,
field::Operand<field::Displacement::Vector, field::FieldOperation::Gradient>>>
);
STATIC_REQUIRE(
std::same_as<
typename field::Displacement::Form::GravityForce::Operands,
field::TypeList<
field::Operand<field::Density::Scalar>, field::Operand<field::Gravity::Flux>,
field::Operand<field::Displacement::Vector, field::FieldOperation::Gradient>,
field::Operand<field::Displacement::Vector>>>
);
STATIC_REQUIRE(field::Displacement::Form::CentrifugalForce::dynamicOrderCount == 1);
STATIC_REQUIRE(field::Displacement::Form::CentrifugalForce::policyKey == quadrature::Term::centrifugal);
CHECK(true);
}
TEST_CASE(
"Enthalpy Registry Preserves Continuous Stellar Field And Coupled Forms",
tags::unit &tags::field
) {
namespace field = mean_field::field;
namespace quadrature = mean_field::quadrature;
CHECK(field::Enthalpy::name == std::string_view{"specific_enthalpy"});
CHECK(field::Enthalpy::Scalar::symbol == std::string_view{"h"});
STATIC_REQUIRE(field::Enthalpy::scalarOrder == 3);
STATIC_REQUIRE(field::Enthalpy::Scalar::rankValue == 0);
STATIC_REQUIRE(std::same_as<typename field::Enthalpy::Scalar::Space, field::H1>);
STATIC_REQUIRE(field_registry_test_utils::typeListSize<field::Enthalpy::FormList> == 9);
STATIC_REQUIRE(field::typeListContains<field::Enthalpy::Form::EosClosureSource, field::Enthalpy::FormList>);
STATIC_REQUIRE(field::typeListContains<field::Enthalpy::Form::EquilibriumEnthalpy, field::Enthalpy::FormList>);
STATIC_REQUIRE(field::typeListContains<field::Enthalpy::Form::EquilibriumGravity, field::Enthalpy::FormList>);
STATIC_REQUIRE(field::typeListContains<field::Enthalpy::Form::EquilibriumRotation, field::Enthalpy::FormList>);
STATIC_REQUIRE(field::typeListContains<field::Enthalpy::Form::EquilibriumConstant, field::Enthalpy::FormList>);
STATIC_REQUIRE(field::typeListContains<field::Enthalpy::Form::IsobaricSurface, field::Enthalpy::FormList>);
STATIC_REQUIRE(field::typeListContains<field::Enthalpy::Form::PressureIntegral, field::Enthalpy::FormList>);
STATIC_REQUIRE(field::typeListContains<field::Enthalpy::Form::PressureForce, field::Enthalpy::FormList>);
STATIC_REQUIRE(field::typeListContains<field::Enthalpy::Form::ErrorNorm, field::Enthalpy::FormList>);
STATIC_REQUIRE(field::Enthalpy::Form::EosClosureSource::dynamicOrderCount == 1);
STATIC_REQUIRE(
std::same_as<
typename field::Enthalpy::Form::EosClosureSource::Operands,
field::TypeList<field::Operand<field::Enthalpy::Scalar>, field::Operand<field::Density::Scalar>>>
);
STATIC_REQUIRE(
std::same_as<
typename field::Enthalpy::Form::EquilibriumGravity::Operands,
field::TypeList<field::Operand<field::Gravity::Potential>, field::Operand<field::Enthalpy::Scalar>>>
);
STATIC_REQUIRE(
std::same_as<
typename field::Enthalpy::Form::EquilibriumConstant::Operands,
field::TypeList<field::Operand<field::BarotropicConstant::Scalar>, field::Operand<field::Enthalpy::Scalar>>>
);
STATIC_REQUIRE(field::Enthalpy::Form::IsobaricSurface::policyKey == quadrature::Term::isobaric_surface);
STATIC_REQUIRE(field::Enthalpy::Form::PressureIntegral::policyKey == quadrature::Term::pressure_integral);
STATIC_REQUIRE(field::Enthalpy::Form::PressureForce::policyKey == quadrature::Term::pressure_force);
STATIC_REQUIRE(
std::same_as<
typename field::Enthalpy::Form::PressureForce::Operands,
field::TypeList<
field::Operand<field::Enthalpy::Scalar>,
field::Operand<field::Displacement::Vector, field::FieldOperation::Gradient>>>
);
CHECK(true);
}
TEST_CASE(
"Barotropic Constant Registry Is A Non Spatial Unit Sized Scalar",
tags::unit &tags::field
) {
namespace field = mean_field::field;
CHECK(field::BarotropicConstant::name == std::string_view{"barotropic_constant"});
CHECK(field::BarotropicConstant::Scalar::symbol == std::string_view{"C"});
STATIC_REQUIRE(field::GlobalScalarQuantity<field::BarotropicConstant::Scalar>);
STATIC_REQUIRE(field::BarotropicConstant::Scalar::staticBlockSize == 1);
STATIC_REQUIRE(field_registry_test_utils::typeListSize<field::BarotropicConstant::Quantities> == 1);
STATIC_REQUIRE(field_registry_test_utils::typeListSize<field::BarotropicConstant::Constraints> == 0);
STATIC_REQUIRE(field_registry_test_utils::typeListSize<field::BarotropicConstant::FormList> == 0);
STATIC_REQUIRE(field::BarotropicConstant::constraintsAreValid);
CHECK(true);
}