Files
MeanField_SERiF/tests/model/reconstruction.cpp
Emily Boudreaux cb20faeb3d test(tests): added test suite
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.
2026-11-09 13:53:17 -05:00

120 lines
6.9 KiB
C++

#include <cmath>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include "serif/model/finite_elements.hpp"
#include "serif/backend/mfem/discretization/field_values.hpp"
#include "serif/backend/mfem/serialize/discretization.hpp"
#include "../sandbox/polytropic_model.hpp"
TEST_CASE("Stellar factory selects compatible surface layers and preserves the ordinary default", "[model][reconstruction]") {
namespace reconstruction = serif::discretization::reconstruction;
namespace blocks = serif::discretization::blocks;
namespace model = serif::model;
using Density = blocks::density::mass;
const auto star = serif::examples::make_polytropic_model(4.0 / 3.0);
const auto ordinary = model::make_stellar_discretization(star);
CHECK_FALSE(ordinary.binding<Density>().reconstruction());
const auto identity = model::make_stellar_discretization(star, {}, reconstruction::SurfacePower<Density>{.density_exponent = 1.0});
CHECK_FALSE(identity.binding<Density>().reconstruction());
for (const int equatorial : {0, 2}) {
const auto weighted = model::make_stellar_discretization(star,
{.surface_refinement = {.levels = 1, .equatorial_levels = equatorial, .equatorial_half_angle_degrees = 10.0}},
reconstruction::SurfacePower<Density>{.density_exponent = 4.0 / 3.0,
.region = reconstruction::SurfaceAdjacent<serif::discretization::domain::StellarSurfaceBoundary>{}});
const auto& density = weighted.binding<Density>();
REQUIRE(density.reconstruction());
int selected = 0, ordinary_material = 0;
for (int element = 0; element < density.mesh().GetNE(); ++element) {
if (!density.supports(element)) { continue; }
if (density.reconstruction_element(element)) { ++selected; }
else { ++ordinary_material; }
}
CHECK(selected > 0);
CHECK(ordinary_material > 0);
CHECK_FALSE(weighted.binding<blocks::gravity::potential>().reconstruction());
}
CHECK_THROWS_WITH((model::make_stellar_discretization(star, {}, reconstruction::SurfacePower<blocks::gravity::potential>{})),
Catch::Matchers::ContainsSubstring("only scalar H1 mass density"));
CHECK_THROWS_WITH((model::make_stellar_discretization(star, {}, reconstruction::SurfacePower<Density>{}, reconstruction::SurfacePower<Density>{})),
Catch::Matchers::ContainsSubstring("at most one"));
}
TEST_CASE("Weighted archives preserve between-node density and reject missing interpretation", "[model][reconstruction]") {
namespace reconstruction = serif::discretization::reconstruction;
namespace backend = serif::backend::mfem::serialize;
namespace discretization = serif::discretization::backend::mfem;
using Density = serif::discretization::blocks::density::mass;
const auto star = serif::examples::make_polytropic_model(5.0 / 3.0);
const auto original = serif::model::make_stellar_discretization(star, {}, reconstruction::SurfacePower<Density>{.density_exponent = 5.0 / 3.0});
const auto record = backend::save_discretization(original);
auto restored = backend::restore_discretization<decltype(original)>(record);
INFO((restored ? "restored" : restored.error().message));
REQUIRE(restored);
CHECK(backend::save_discretization(*restored) == record);
const auto& binding = original.binding<Density>();
::mfem::Vector coefficients(binding.size());
for (int i = 0; i < coefficients.Size(); ++i) { coefficients[i] = std::sin(0.13 * i); }
discretization::FieldValues<Density> before(binding, coefficients), after(restored->binding<Density>(), coefficients);
::mfem::IntegrationPoint point;
point.Set3(0.17, 0.31, 0.87);
for (int element = 0; element < binding.mesh().GetNE(); ++element) {
if (!binding.supports(element)) { continue; }
CHECK(before.value(element, point) == Catch::Approx(after.value(element, point)).margin(1e-13));
}
auto malformed = record;
for (auto& [key, saved] : malformed.children.at("bindings").children) {
if (saved.children.contains("reconstruction")) {
CHECK(saved.version == 2);
saved.children.erase("reconstruction");
}
}
CHECK_FALSE(backend::restore_discretization<decltype(original)>(malformed));
}
TEST_CASE("Reconstruction plans reject attachment after the boundary changes", "[model][reconstruction]") {
namespace reconstruction = serif::discretization::reconstruction;
using Density = serif::discretization::blocks::density::mass;
const auto star = serif::examples::make_polytropic_model(1.5);
const auto finite_elements = serif::model::make_stellar_discretization(star, {}, reconstruction::SurfacePower<Density>{.density_exponent = 1.5});
const auto& density = finite_elements.binding<Density>();
const auto plan = density.reconstruction();
auto& mesh = density.mesh();
REQUIRE(mesh.GetNBE() > 0);
mesh.SetBdrAttribute(0, mesh.GetBdrAttribute(0) + 10);
using Binding = std::remove_cvref_t<decltype(density)>;
CHECK_THROWS_WITH(Binding(density.space(), plan), Catch::Matchers::ContainsSubstring("Rebuild the reconstruction plan"));
CHECK_THROWS(density.validate());
}
TEST_CASE("Reconstruction rejects radial hanging layers and stale refined-space plans", "[model][reconstruction]") {
namespace backend = serif::discretization::backend::mfem;
namespace reconstruction = serif::discretization::reconstruction;
using Density = serif::discretization::blocks::density::mass;
using Schema = serif::discretization::domain::schema::CoreEnvelopeVacuumDomainSchema;
auto mesh = ::mfem::Mesh::MakeCartesian3D(2, 1, 1, ::mfem::Element::HEXAHEDRON);
::mfem::Array<int> vertices;
for (int boundary = 0; boundary < mesh.GetNBE(); ++boundary) {
mesh.GetBdrElementVertices(boundary, vertices);
bool surface = true;
for (int vertex : vertices) { surface = surface && mesh.GetVertex(vertex)[2] == 0.0; }
mesh.GetBdrElement(boundary)->SetAttribute(surface ? 1 : 2);
}
mesh.SetAttributes();
::mfem::H1_FECollection collection(3, 3);
::mfem::FiniteElementSpace space(&mesh, &collection);
backend::TermBinding<Density, Schema> original(space);
const auto specification = reconstruction::SurfacePower<Density>{.density_exponent = 1.5};
const auto old_plan = backend::make_reconstruction(specification, original);
::mfem::Array<::mfem::Refinement> refinement;
refinement.Append(::mfem::Refinement(0, ::mfem::Refinement::Z));
mesh.GeneralRefinement(refinement, 1);
space.Update();
CHECK_THROWS_WITH((backend::TermBinding<Density, Schema>(space, old_plan)),
Catch::Matchers::ContainsSubstring("Rebuild the reconstruction plan"));
backend::TermBinding<Density, Schema> updated(space);
CHECK_THROWS_WITH(backend::make_reconstruction(specification, updated),
Catch::Matchers::ContainsSubstring("matching radial layers"));
}