The test suite is extensive. Note that LLMs were used to aid in the development of the test suite. All LLM generated code was manually reviewed.
211 lines
11 KiB
C++
211 lines
11 KiB
C++
#include <algorithm>
|
|
#include <cmath>
|
|
#include <memory>
|
|
#include <numbers>
|
|
#include <vector>
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
|
#include "mfem/fem/coefficient.hpp"
|
|
#include "mfem/fem/gridfunc.hpp"
|
|
#include "serif/tests/equations/test_equations.hpp"
|
|
#include "serif/backend/mfem/discretization/boundary_trace.hpp"
|
|
#include "serif/discretization/blocks/fields.hpp"
|
|
#include "serif/backend/mfem/geometry/deformation/inputs.hpp"
|
|
#include "serif/backend/mfem/geometry/deformation/reference_geometry.hpp"
|
|
#include "serif/backend/mfem/geometry/deformation/surface_metric.hpp"
|
|
#include "serif/tests/deformation/mesh.hpp"
|
|
|
|
namespace
|
|
{
|
|
namespace deformation = serif::geometry::deformation;
|
|
namespace backend = deformation::backend::mfem;
|
|
namespace finite_elements = serif::discretization::backend::mfem;
|
|
namespace domain = serif::discretization::domain;
|
|
using Schema = domain::schema::CoreEnvelopeVacuumDomainSchema;
|
|
using Catch::Matchers::WithinAbs;
|
|
using Catch::Matchers::WithinRel;
|
|
|
|
/** @brief Retain the original eight-coordinate indexing example without imposing a higher-order physical field. */
|
|
struct LinearEnthalpy : serif::discretization::blocks::term<::serif::tests::equations::enthalpy>
|
|
{
|
|
using FunctionalSpace = serif::discretization::functional_space::DiscreteFunctionalSpace<serif::discretization::functional_space::H1, 1, 0>;
|
|
using Support = domain::StellarDomains;
|
|
using Quantity = serif::dimensions::SpecificEnthalpy;
|
|
struct value : serif::discretization::blocks::value_block_base
|
|
{
|
|
};
|
|
struct residual : serif::discretization::blocks::residual_block_base
|
|
{
|
|
};
|
|
};
|
|
|
|
using AlternateBoundarySchema = domain::schema::DomainSchema<typename Schema::domain_ids_type, domain::ids::lists::BoundaryIDList<domain::ids::BoundaryID<domain::StellarSurfaceBoundary, 2>, domain::ids::BoundaryID<domain::InfinitySurfaceBoundary, 1>>, domain::relation::lists::RelationList<>>;
|
|
/** @brief Resolve boundary coordinates independently using MFEM's serial essential true-DOF list. */
|
|
[[nodiscard]] std::vector<int> essential_dofs(const mfem::FiniteElementSpace& space, const int attribute)
|
|
{
|
|
mfem::Array<int> marker(space.GetMesh()->bdr_attributes.Max()), selected;
|
|
marker = 0;
|
|
marker[attribute - 1] = 1;
|
|
space.GetEssentialTrueDofs(marker, selected);
|
|
return std::vector<int>(selected.begin(), selected.end());
|
|
}
|
|
} // namespace
|
|
|
|
TEST_CASE("MeanField canonical scalar coordinates retain exact gather and transpose scatter indexing", "[unit][deformation][meanfield]")
|
|
{
|
|
mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D(1, 1, 1, mfem::Element::HEXAHEDRON);
|
|
mfem::H1_FECollection collection(1, 3);
|
|
mfem::FiniteElementSpace space(&mesh, &collection);
|
|
finite_elements::TermBinding<LinearEnthalpy, Schema> binding(space);
|
|
REQUIRE(binding.size() == 8);
|
|
const mfem::Array<int> selected{1, 3, 6};
|
|
const auto rows = std::make_shared<const finite_elements::DofSelection>(binding, selected);
|
|
const auto trace = finite_elements::make_boundary_trace(rows, binding);
|
|
mfem::Vector full(binding.size()), sampled(rows->size()), scattered(binding.size());
|
|
for (int dof = 0; dof < full.Size(); ++dof)
|
|
{
|
|
full[dof] = 10.0 + dof;
|
|
}
|
|
trace.interpolation().Mult(full, sampled);
|
|
CHECK(sampled[0] == 11.0);
|
|
CHECK(sampled[1] == 13.0);
|
|
CHECK(sampled[2] == 16.0);
|
|
trace.interpolation().MultTranspose(sampled, scattered);
|
|
for (int dof = 0; dof < scattered.Size(); ++dof)
|
|
{
|
|
CHECK(scattered[dof] == (dof == 1 || dof == 3 || dof == 6 ? full[dof] : 0.0));
|
|
}
|
|
CHECK_THROWS_AS((finite_elements::DofSelection{binding, mfem::Array<int>{3, 1}}), std::invalid_argument);
|
|
CHECK_THROWS_AS((finite_elements::DofSelection{binding, mfem::Array<int>{1, 1}}), std::invalid_argument);
|
|
CHECK_THROWS_AS((finite_elements::DofSelection{binding, mfem::Array<int>{-1}}), std::invalid_argument);
|
|
CHECK_THROWS_AS((finite_elements::DofSelection{binding, mfem::Array<int>{8}}), std::invalid_argument);
|
|
}
|
|
|
|
TEST_CASE("MeanField surface coordinates resolve semantic boundary IDs and scalar companion nodes", "[unit][deformation][meanfield]")
|
|
{
|
|
serif::tests::deformation::PairedMesh fixture(0, 0.1, 3, 4);
|
|
const auto primary = backend::deformation_discretization<Schema>(fixture.scalar_space, fixture.vector_space, *fixture.generated.reference_mesh);
|
|
const auto alternate = backend::deformation_discretization<AlternateBoundarySchema>(fixture.scalar_space, fixture.vector_space, *fixture.generated.reference_mesh);
|
|
const auto stellar = backend::sample_reference_surface(fixture.scalar_space, primary.surface_attribute);
|
|
const auto infinity = backend::select_surface_scalar_dofs(fixture.scalar_space, alternate.surface_attribute);
|
|
REQUIRE(stellar);
|
|
REQUIRE(infinity);
|
|
CHECK(stellar->scalar_dofs == essential_dofs(fixture.scalar_space, 1));
|
|
CHECK(*infinity == essential_dofs(fixture.scalar_space, 2));
|
|
CHECK(stellar->scalar_dofs != *infinity);
|
|
CHECK(std::is_sorted(stellar->scalar_dofs.begin(), stellar->scalar_dofs.end()));
|
|
REQUIRE(stellar->scalar_dofs.size() > 0);
|
|
CHECK(stellar->scalar_dofs.size() < static_cast<std::size_t>(fixture.scalar_space.GetTrueVSize()));
|
|
const auto repeated = backend::select_surface_scalar_dofs(fixture.scalar_space, primary.surface_attribute);
|
|
REQUIRE(repeated);
|
|
CHECK(*repeated == stellar->scalar_dofs);
|
|
|
|
for (const mfem::Ordering::Type ordering : {mfem::Ordering::byNODES, mfem::Ordering::byVDIM})
|
|
{
|
|
mfem::FiniteElementSpace vector_space(fixture.generated.mesh.get(), &fixture.collection, 3, ordering);
|
|
const std::vector<int> vector_dofs = essential_dofs(vector_space, primary.surface_attribute);
|
|
CHECK(vector_dofs.size() == 3 * stellar->scalar_dofs.size());
|
|
mfem::VectorFunctionCoefficient coordinates(3, [](const mfem::Vector& position, mfem::Vector& value)
|
|
{
|
|
value = position;
|
|
});
|
|
mfem::GridFunction projected(&vector_space);
|
|
projected.ProjectCoefficient(coordinates);
|
|
for (std::size_t node = 0; node < stellar->positions.size(); ++node)
|
|
{
|
|
for (int component = 0; component < 3; ++component)
|
|
{
|
|
CHECK_THAT(stellar->positions[node][component], WithinAbs(projected[vector_space.DofToVDof(stellar->scalar_dofs[node], component)], 2.0e-13));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("MeanField stellar surface metrics include tagged internal interfaces", "[unit][deformation][meanfield]")
|
|
{
|
|
serif::tests::deformation::PairedMesh fixture;
|
|
int selected_faces = 0;
|
|
for (int boundary = 0; boundary < fixture.generated.mesh->GetNBE(); ++boundary)
|
|
{
|
|
if (fixture.generated.mesh->GetBdrAttribute(boundary) == fixture.surface_attribute)
|
|
{
|
|
const int face = fixture.generated.mesh->GetBdrElementFaceIndex(boundary);
|
|
REQUIRE(face >= 0);
|
|
CHECK(fixture.generated.mesh->GetFaceInformation(face).IsOfFaceType(mfem::FaceType::Interior));
|
|
++selected_faces;
|
|
}
|
|
}
|
|
REQUIRE(selected_faces > 0);
|
|
const auto reference = backend::sample_reference_surface(fixture.scalar_space, fixture.surface_attribute);
|
|
REQUIRE(reference);
|
|
const auto embedding = deformation::build_surface_embedding(deformation::NodalRadialSurface{}, reference->positions);
|
|
REQUIRE(embedding);
|
|
const auto metric = backend::surface_parameter_mass_diagonal(fixture.scalar_space, fixture.surface_attribute, *reference, *embedding);
|
|
REQUIRE(metric);
|
|
REQUIRE(metric->size() == reference->scalar_dofs.size());
|
|
for (const double diagonal : *metric)
|
|
{
|
|
CHECK(std::isfinite(diagonal));
|
|
CHECK(diagonal > 0.0);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("MeanField STROID spherical volume retains its original mesh accuracy", "[integration][deformation][geometry][meanfield]")
|
|
{
|
|
// Match sandbox.smesh: two refinements, quartic geometry, stellar radius 1 and reference infinity 5.
|
|
auto generated = serif::tests::deformation::make_mesh(2, 0.0, 4, 5.0);
|
|
long double volume = 0.0L;
|
|
int stellar_elements = 0;
|
|
for (int element = 0; element < generated.mesh->GetNE(); ++element)
|
|
{
|
|
if (!Schema::domain_id_belongs_to<domain::StellarDomains>(generated.mesh->GetAttribute(element)))
|
|
{
|
|
continue;
|
|
}
|
|
mfem::ElementTransformation& transformation = *generated.mesh->GetElementTransformation(element);
|
|
const mfem::IntegrationRule& rule = mfem::IntRules.Get(transformation.GetGeometryType(), transformation.OrderW());
|
|
for (int point = 0; point < rule.GetNPoints(); ++point)
|
|
{
|
|
const mfem::IntegrationPoint& sample = rule.IntPoint(point);
|
|
transformation.SetIntPoint(&sample);
|
|
volume += sample.weight * transformation.Weight();
|
|
}
|
|
++stellar_elements;
|
|
}
|
|
REQUIRE(stellar_elements > 0);
|
|
CHECK_THAT(static_cast<double>(volume), WithinRel(4.0 * std::numbers::pi / 3.0, 1.0e-6));
|
|
}
|
|
|
|
TEST_CASE("Shared MFEM reference tracking distinguishes coordinate and label dependencies", "[unit][deformation][reference_tracking]") {
|
|
namespace shared = serif::backend::mfem::shared;
|
|
mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D(1, 1, 1, mfem::Element::HEXAHEDRON);
|
|
mfem::H1_FECollection collection(1, 3);
|
|
mfem::FiniteElementSpace space(&mesh, &collection);
|
|
const auto topology = shared::mesh_snapshot(&mesh, false, false);
|
|
const auto materials = shared::mesh_snapshot(&mesh, false, true);
|
|
const auto coordinates = shared::mesh_snapshot(&mesh, true, true);
|
|
const auto original_space = shared::space_snapshot(space);
|
|
REQUIRE(shared::mesh_matches(topology, &mesh));
|
|
REQUIRE(shared::mesh_matches(coordinates, &mesh));
|
|
|
|
mesh.Transform([](const mfem::Vector& position, mfem::Vector& transformed) {
|
|
transformed = position;
|
|
transformed[0] *= 1.2;
|
|
});
|
|
CHECK(shared::mesh_matches(topology, &mesh));
|
|
CHECK(shared::mesh_matches(materials, &mesh));
|
|
CHECK_FALSE(shared::mesh_matches(coordinates, &mesh));
|
|
CHECK(shared::space_snapshot(space) == original_space);
|
|
|
|
mesh.GetElement(0)->SetAttribute(9);
|
|
CHECK(shared::mesh_matches(topology, &mesh));
|
|
CHECK_FALSE(shared::mesh_matches(materials, &mesh));
|
|
mesh.GetBdrElement(0)->SetAttribute(9);
|
|
CHECK_FALSE(shared::mesh_matches(topology, &mesh));
|
|
mfem::FiniteElementSpace other(&mesh, &collection);
|
|
CHECK_FALSE(shared::space_snapshot(other) == original_space);
|
|
CHECK_FALSE(shared::mesh_matches(topology, nullptr));
|
|
CHECK(shared::mesh_matches(shared::mesh_snapshot(nullptr, true, true), nullptr));
|
|
}
|