#include #include #include #include #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().reconstruction()); const auto identity = model::make_stellar_discretization(star, {}, reconstruction::SurfacePower{.density_exponent = 1.0}); CHECK_FALSE(identity.binding().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_exponent = 4.0 / 3.0, .region = reconstruction::SurfaceAdjacent{}}); const auto& density = weighted.binding(); 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().reconstruction()); } CHECK_THROWS_WITH((model::make_stellar_discretization(star, {}, reconstruction::SurfacePower{})), Catch::Matchers::ContainsSubstring("only scalar H1 mass density")); CHECK_THROWS_WITH((model::make_stellar_discretization(star, {}, reconstruction::SurfacePower{}, reconstruction::SurfacePower{})), 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_exponent = 5.0 / 3.0}); const auto record = backend::save_discretization(original); auto restored = backend::restore_discretization(record); INFO((restored ? "restored" : restored.error().message)); REQUIRE(restored); CHECK(backend::save_discretization(*restored) == record); const auto& binding = original.binding(); ::mfem::Vector coefficients(binding.size()); for (int i = 0; i < coefficients.Size(); ++i) { coefficients[i] = std::sin(0.13 * i); } discretization::FieldValues before(binding, coefficients), after(restored->binding(), 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(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_exponent = 1.5}); const auto& density = finite_elements.binding(); 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; 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 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 original(space); const auto specification = reconstruction::SurfacePower{.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(space, old_plan)), Catch::Matchers::ContainsSubstring("Rebuild the reconstruction plan")); backend::TermBinding updated(space); CHECK_THROWS_WITH(backend::make_reconstruction(specification, updated), Catch::Matchers::ContainsSubstring("matching radial layers")); }