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.
This commit is contained in:
366
tests/serialize/top_mfem.cpp
Normal file
366
tests/serialize/top_mfem.cpp
Normal file
@@ -0,0 +1,366 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <numbers>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "serif/backend/mfem/discretization/field_values.hpp"
|
||||
#include "serif/backend/mfem/serialize/resources.hpp"
|
||||
#include "serif/backend/mfem/serialize/top.hpp"
|
||||
#include "serif/backend/mfem/solver/seed/state.hpp"
|
||||
#include "serif/model/finite_elements.hpp"
|
||||
#include "serif/solver/seed/polytrope.hpp"
|
||||
#include "serif/tests/solver/stellar_fixture.hpp"
|
||||
#include "../sandbox/stellar_rayfem.hpp"
|
||||
|
||||
namespace {
|
||||
namespace fixture = serif::tests::solver;
|
||||
namespace serialization = serif::utils::serialize;
|
||||
namespace native_archive = serif::backend::mfem::serialize;
|
||||
namespace seed = serif::solver::seed;
|
||||
|
||||
/** @brief Supply the exact zero displacement and accepted shape used by a manufactured display state. */
|
||||
struct StationaryDisplayCache {
|
||||
struct ShapeSnapshot { std::span<const double> parameters; } linearization;
|
||||
::mfem::Vector displacement;
|
||||
|
||||
/** @brief Expose only the two accepted-state records consumed by the display adapter. */
|
||||
template <typename Key>
|
||||
[[nodiscard]] const auto& get() const {
|
||||
if constexpr (std::same_as<Key, serif::geometry::deformation::keys::Linearization>) {
|
||||
return linearization;
|
||||
} else {
|
||||
static_assert(std::same_as<Key, serif::geometry::deformation::backend::mfem::volume_displacement_keys::Coefficients>);
|
||||
return displacement;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** @brief Keep successful exports and rejected partial files within a test's lifetime. */
|
||||
struct ExportDirectory {
|
||||
std::filesystem::path path = std::filesystem::temp_directory_path() /
|
||||
("serif-top-mfem-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||
~ExportDirectory() { std::error_code error; std::filesystem::remove_all(path, error); }
|
||||
};
|
||||
|
||||
/** @brief Small sampling grids test the adapter; their coarse P2 density is not a seismic accuracy benchmark. */
|
||||
serialization::TopExportOptions small_export() {
|
||||
return {.radial_points = 12, .maximum_degree = 4, .angular_points = 12, .azimuthal_points = 4,
|
||||
.relative_symmetry_tolerance = 3.0e-2, .relative_surface_tolerance = 1.0e-6,
|
||||
.relative_surface_projection_tolerance = 1.0e-3};
|
||||
}
|
||||
|
||||
/** @brief Evaluate the original FE field at the center independently of the exporter and seed scales. */
|
||||
template <typename Discretization, typename State>
|
||||
double central_density(const Discretization& finite_elements, const State& state) {
|
||||
const auto& binding = finite_elements.template binding<fixture::Density>();
|
||||
const auto values = state.template get<fixture::Density>();
|
||||
::mfem::Vector reduced(static_cast<int>(values.size()));
|
||||
std::ranges::copy(values, reduced.GetData());
|
||||
::mfem::GridFunction density(&binding.space());
|
||||
density = binding.scatter(reduced);
|
||||
::mfem::Vector center(3);
|
||||
center = 0.0;
|
||||
for (int element = 0; element < binding.mesh().GetNE(); ++element) {
|
||||
if (!binding.supports(element)) continue;
|
||||
::mfem::InverseElementTransformation inverse(binding.mesh().GetElementTransformation(element));
|
||||
::mfem::IntegrationPoint point;
|
||||
if (inverse.Transform(center, point) == ::mfem::InverseElementTransformation::Inside)
|
||||
return density.GetValue(element, point);
|
||||
}
|
||||
FAIL("The independently sampled stellar center must lie in a material element.");
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
/** @brief Input preservation includes signed zero, as well as all physical and reference mesh coordinates. */
|
||||
template <typename State>
|
||||
void check_state_unchanged(const State& state, const std::vector<double>& original) {
|
||||
REQUIRE(state.values().size() == original.size());
|
||||
CHECK(std::ranges::equal(state.values(), original, [](double a, double b) {
|
||||
return std::bit_cast<std::uint64_t>(a) == std::bit_cast<std::uint64_t>(b);
|
||||
}));
|
||||
}
|
||||
|
||||
/** @brief Read the TOP scalar file independently of the serializer's returned report. */
|
||||
std::array<double, 9> scalar_file(const std::filesystem::path& directory) {
|
||||
std::ifstream input(directory / "lambda_eps");
|
||||
REQUIRE(input.good());
|
||||
std::array<double, 9> values{};
|
||||
for (double& value : values) {
|
||||
REQUIRE(static_cast<bool>(input >> value));
|
||||
REQUIRE(std::isfinite(value));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("TOP exports the current deformed FE star without changing its state or either mesh", "[serialize][top_export][mfem]") {
|
||||
constexpr double G = 0.7, K = 0.3, mass = 1.4;
|
||||
const auto model = fixture::make_model(1.0, K, mass, 0.03, G);
|
||||
const auto radial = seed::make_polytropic_seed(model);
|
||||
REQUIRE(radial);
|
||||
const auto resources = fixture::make_resources(radial->scales.radius.value());
|
||||
const auto finite_elements = fixture::make_discretization(resources, 0);
|
||||
auto state = seed::backend::mfem::make_polytropic_state(model, finite_elements, *radial);
|
||||
REQUIRE(state);
|
||||
// This is a valid manufactured state, not a claimed equilibrium. A nonzero
|
||||
// monopole makes using the undeformed source mesh detectably wrong.
|
||||
state->template get<fixture::Shape>()[0] = 0.02 * radial->scales.radius.value();
|
||||
const double rho_c = central_density(finite_elements, *state);
|
||||
REQUIRE(rho_c > 0.0);
|
||||
const std::vector<double> original_state(state->values().begin(), state->values().end());
|
||||
const auto original_mesh = native_archive::mesh_text(*resources->mesh.mesh);
|
||||
const auto original_reference = native_archive::mesh_text(*resources->mesh.reference_mesh);
|
||||
const ExportDirectory output;
|
||||
const auto result = serialization::export_top(output.path, model, finite_elements, *state, small_export());
|
||||
INFO((result ? "Export succeeded" : result.error().path + ": " + result.error().message));
|
||||
check_state_unchanged(*state, original_state);
|
||||
CHECK(native_archive::mesh_text(*resources->mesh.mesh) == original_mesh);
|
||||
CHECK(native_archive::mesh_text(*resources->mesh.reference_mesh) == original_reference);
|
||||
REQUIRE(result);
|
||||
CHECK(result->equatorial_radius > radial->scales.radius.value() * 1.003);
|
||||
CHECK(result->polar_radius == Catch::Approx(result->equatorial_radius).epsilon(5.0e-4));
|
||||
CHECK(result->angular_frequency_unit == Catch::Approx(std::sqrt(4.0 * std::numbers::pi * G * rho_c)).epsilon(2.0e-10));
|
||||
// At n=1 the density cancels: this tests recovery of both physical G and K.
|
||||
CHECK(result->lambda == Catch::Approx(4.0 * std::numbers::pi * G * std::pow(result->equatorial_radius, 2) / (2.0 * K)).epsilon(2.0e-12));
|
||||
const auto written = scalar_file(output.path);
|
||||
CHECK(written[0] == result->lambda);
|
||||
CHECK(written[2] == result->alpha);
|
||||
CHECK(written[4] == 1.0);
|
||||
CHECK(written[8] == small_export().radial_points);
|
||||
CHECK(written[7] / std::sqrt(3.0 * written[2]) ==
|
||||
Catch::Approx(state->template get<fixture::Omega>()[0] / result->angular_frequency_unit).epsilon(2.0e-12));
|
||||
CHECK(std::filesystem::file_size(output.path / "enthalpy_et_der") > 0);
|
||||
CHECK(std::filesystem::file_size(output.path / "domains_boundaries") > 0);
|
||||
// The EOS and current density determine the export. A different unsatisfied
|
||||
// mass prescription must not change TOP's background normalization.
|
||||
const auto other_mass = fixture::make_model(1.0, K, 3.0 * mass, 0.03, G);
|
||||
const ExportDirectory other_output;
|
||||
const auto other = serialization::export_top(other_output.path, other_mass, finite_elements, *state, small_export());
|
||||
INFO((other ? "Changed-prescription export succeeded" : other.error().path + ": " + other.error().message));
|
||||
REQUIRE(other);
|
||||
CHECK(other->alpha == result->alpha);
|
||||
CHECK(other->rotation_over_kepler == result->rotation_over_kepler);
|
||||
check_state_unchanged(*state, original_state);
|
||||
CHECK(native_archive::mesh_text(*resources->mesh.mesh) == original_mesh);
|
||||
CHECK(native_archive::mesh_text(*resources->mesh.reference_mesh) == original_reference);
|
||||
}
|
||||
|
||||
TEST_CASE("TOP sampling honors a tilted rotation axis on a surface and equator refined true-DOF mesh", "[serialize][top_export][mfem][nonconforming]") {
|
||||
namespace model = serif::model;
|
||||
namespace deformation = serif::geometry::deformation;
|
||||
const auto base = fixture::make_model();
|
||||
const auto star = model::make_stellar_model(base.equation_of_state(), base.equations(),
|
||||
model::FixedTotalMass{serif::dimensions::MassValue{1.0}},
|
||||
model::FixedAngularMomentum{model::FixedAngularMomentum::Parameters{
|
||||
.angular_momentum = serif::dimensions::AngularMomentumValue{0.04}, .axis = {1.0, 2.0, 3.0}}});
|
||||
const auto& axis = star.specification<model::FixedAngularMomentum>().axis();
|
||||
const auto radial = seed::make_polytropic_seed(star);
|
||||
REQUIRE(radial);
|
||||
const auto finite_elements = model::make_stellar_discretization(star,
|
||||
{.radius = radial->scales.radius, .geometry_order = 6,
|
||||
.surface_refinement = {.levels = 1, .equatorial_levels = 1, .axis = axis}},
|
||||
deformation::DomainDeformation<deformation::SphericalHarmonicSurface>{
|
||||
.surface = {.maximum_degree = 0}, .stellar_interior = {}, .vacuum = {}});
|
||||
const auto& binding = finite_elements.template binding<fixture::Density>();
|
||||
REQUIRE(binding.space().GetTrueVSize() < binding.space().GetVSize());
|
||||
REQUIRE(binding.constrained());
|
||||
auto state = seed::backend::mfem::make_polytropic_state(star, finite_elements, *radial);
|
||||
REQUIRE(state);
|
||||
// A positive quadrupolar perturbation makes the chosen axis observable:
|
||||
// treating this field as axisymmetric about z must not silently pass.
|
||||
const double radius = radial->scales.radius.value();
|
||||
::mfem::FunctionCoefficient prescribed_density([&](const ::mfem::Vector& position) {
|
||||
const double r = position.Norml2();
|
||||
if (r >= radius) return 0.0;
|
||||
const double x = std::numbers::pi * r / radius;
|
||||
const double profile = (x == 0.0 ? 1.0 : std::sin(x) / x) * radial->scales.central_density.value();
|
||||
double axial = 0.0;
|
||||
for (int component = 0; component < 3; ++component) axial += axis[component] * position[component];
|
||||
return profile * (1.0 + 0.6 * (3.0 * axial * axial - r * r) / (radius * radius));
|
||||
});
|
||||
::mfem::GridFunction density(&binding.space());
|
||||
density.ProjectCoefficient(prescribed_density);
|
||||
const auto reduced = binding.gather(density);
|
||||
std::copy_n(reduced.GetData(), reduced.Size(), state->template get<fixture::Density>().begin());
|
||||
const auto trace = serif::discretization::backend::mfem::select_boundary_dofs<
|
||||
serif::discretization::domain::StellarSurfaceBoundary>(binding);
|
||||
for (const int dof : trace.reduced_dofs()) state->template get<fixture::Density>()[dof] = 0.0;
|
||||
const std::vector<double> original_state(state->values().begin(), state->values().end());
|
||||
const auto original_mesh = native_archive::mesh_text(binding.mesh());
|
||||
const ExportDirectory output;
|
||||
const auto result = serialization::export_top(output.path, star, finite_elements, *state, small_export());
|
||||
INFO((result ? "Export succeeded" : result.error().path + ": " + result.error().message));
|
||||
check_state_unchanged(*state, original_state);
|
||||
CHECK(native_archive::mesh_text(binding.mesh()) == original_mesh);
|
||||
REQUIRE(result);
|
||||
CHECK(result->polar_radius == Catch::Approx(radial->scales.radius.value()).epsilon(5.0e-4));
|
||||
CHECK(result->equatorial_radius == Catch::Approx(radial->scales.radius.value()).epsilon(5.0e-4));
|
||||
CHECK(result->angular_frequency_unit == Catch::Approx(std::sqrt(4.0 * std::numbers::pi * central_density(finite_elements, *state))).epsilon(2.0e-10));
|
||||
const auto written = scalar_file(output.path);
|
||||
CHECK(written[7] / std::sqrt(3.0 * written[2]) ==
|
||||
Catch::Approx(state->template get<fixture::Omega>()[0] / result->angular_frequency_unit).epsilon(2.0e-12));
|
||||
const auto wrong_axis = fixture::make_model(1.0, 2.0 / std::numbers::pi, 1.0, 0.04);
|
||||
const ExportDirectory wrong_output;
|
||||
const auto rejected = serialization::export_top(wrong_output.path, wrong_axis, finite_elements, *state, small_export());
|
||||
REQUIRE_FALSE(rejected);
|
||||
CHECK_FALSE(std::filesystem::exists(wrong_output.path / "lambda_eps"));
|
||||
}
|
||||
|
||||
TEST_CASE("TOP rejects a finite pressure stellar boundary before publishing vacuum polytrope files", "[serialize][top_export][mfem]") {
|
||||
const auto vacuum = fixture::make_model();
|
||||
const auto radial = seed::make_polytropic_seed(vacuum);
|
||||
REQUIRE(radial);
|
||||
const auto resources = fixture::make_resources(radial->scales.radius.value());
|
||||
const auto finite_elements = fixture::make_discretization(resources, 0);
|
||||
const auto state = seed::backend::mfem::make_polytropic_state(vacuum, finite_elements, *radial);
|
||||
REQUIRE(state);
|
||||
const auto pressured = fixture::make_model(1.0, 2.0 / std::numbers::pi, 1.0, 0.0, 1.0, 0.01);
|
||||
const std::vector<double> original_state(state->values().begin(), state->values().end());
|
||||
const auto original_mesh = native_archive::mesh_text(*resources->mesh.mesh);
|
||||
const ExportDirectory output;
|
||||
const auto result = serialization::export_top(output.path, pressured, finite_elements, *state, small_export());
|
||||
REQUIRE_FALSE(result);
|
||||
CHECK(result.error().message.find("pressure") != std::string::npos);
|
||||
CHECK_FALSE(std::filesystem::exists(output.path / "lambda_eps"));
|
||||
check_state_unchanged(*state, original_state);
|
||||
CHECK(native_archive::mesh_text(*resources->mesh.mesh) == original_mesh);
|
||||
}
|
||||
|
||||
TEST_CASE("FieldValues samples reconstructed density and its correctly mapped gradient", "[serialize][mfem][reconstruction]") {
|
||||
namespace discretization = serif::discretization;
|
||||
namespace backend = discretization::backend::mfem;
|
||||
using Density = discretization::blocks::density::mass;
|
||||
using Schema = discretization::domain::schema::CoreEnvelopeVacuumDomainSchema;
|
||||
constexpr double exponent = 4.0 / 3.0;
|
||||
|
||||
::mfem::Mesh mesh = ::mfem::Mesh::MakeCartesian3D(1, 1, 1, ::mfem::Element::HEXAHEDRON, 2.0, 3.0, 4.0);
|
||||
for (int face = 0; face < mesh.GetNBE(); ++face) {
|
||||
auto& transformation = *mesh.GetBdrElementTransformation(face);
|
||||
::mfem::Vector center(3);
|
||||
transformation.Transform(::mfem::Geometries.GetCenter(transformation.GetGeometryType()), center);
|
||||
mesh.GetBdrElement(face)->SetAttribute(center[0] == 0.0 ? 1 : 2);
|
||||
}
|
||||
::mfem::H1_FECollection collection(discretization::blocks::SpatialTermTraits<Density>::order, 3);
|
||||
::mfem::FiniteElementSpace space(&mesh, &collection);
|
||||
backend::TermBinding<Density, Schema> ordinary(space);
|
||||
auto plan = std::make_shared<backend::ReconstructionPlan>(ordinary, 1,
|
||||
std::make_shared<discretization::reconstruction::SurfacePowerProfile>(exponent));
|
||||
backend::TermBinding<Density, Schema> reconstructed(space, plan);
|
||||
::mfem::FunctionCoefficient profile([](const ::mfem::Vector& position) {
|
||||
return std::pow(position[0] / 2.0, exponent) * (1.0 + position[1]) * (2.0 - position[2]);
|
||||
});
|
||||
::mfem::GridFunction nodal(&space);
|
||||
nodal.ProjectCoefficient(profile);
|
||||
const ::mfem::Vector coefficients = reconstructed.gather(nodal);
|
||||
backend::FieldValues<Density> values(reconstructed, coefficients);
|
||||
backend::FieldValues<Density> polynomial(ordinary, coefficients);
|
||||
CHECK_THROWS(backend::FieldValues<discretization::blocks::displacement::vector>(ordinary, coefficients));
|
||||
::mfem::IntegrationPoint point;
|
||||
point.Set3(0.2, 0.3, 0.4);
|
||||
const double expected = std::pow(point.x, exponent) * 1.9 * 0.4;
|
||||
CHECK(values.value(0, point) == Catch::Approx(expected).margin(2.0e-13));
|
||||
CHECK(std::abs(polynomial.value(0, point) - expected) > 1.0e-4);
|
||||
CHECK(polynomial.value(0, point) == Catch::Approx(nodal.GetValue(0, point)).margin(2.0e-13));
|
||||
const auto gradient = values.gradient(0, point);
|
||||
CHECK(gradient[0] == Catch::Approx(exponent * std::pow(point.x, exponent - 1.0) * 1.9 * 0.4 / 2.0).margin(2.0e-13));
|
||||
CHECK(gradient[1] == Catch::Approx(std::pow(point.x, exponent) * 0.4).margin(2.0e-13));
|
||||
CHECK(gradient[2] == Catch::Approx(-std::pow(point.x, exponent) * 1.9).margin(2.0e-13));
|
||||
serif::mapping::MappingPointContext dilation;
|
||||
dilation.inverse_mapping_jacobian.SetSize(3);
|
||||
dilation.inverse_mapping_jacobian = 0.0;
|
||||
for (int axis = 0; axis < 3; ++axis) dilation.inverse_mapping_jacobian(axis, axis) = 0.5;
|
||||
const auto physical_gradient = values.gradient(0, point, dilation);
|
||||
for (int axis = 0; axis < 3; ++axis) CHECK(physical_gradient[axis] == Catch::Approx(0.5 * gradient[axis]).margin(2.0e-13));
|
||||
CHECK(values.value(0, point, dilation) == Catch::Approx(expected).margin(2.0e-13));
|
||||
const auto& points = ::mfem::IntRules.Get(::mfem::Geometry::CUBE, 8);
|
||||
::mfem::Vector batch;
|
||||
values.values(0, points, batch);
|
||||
REQUIRE(batch.Size() == points.GetNPoints());
|
||||
for (int sample = 0; sample < points.GetNPoints(); ++sample) {
|
||||
const auto& at = points.IntPoint(sample);
|
||||
CHECK(batch[sample] == Catch::Approx(std::pow(at.x, exponent) * (1.0 + 3.0 * at.y) * (2.0 - 4.0 * at.z)).margin(2.0e-12));
|
||||
}
|
||||
point.x = 0.0;
|
||||
CHECK(values.value(0, point) == Catch::Approx(0.0).margin(2.0e-13));
|
||||
for (double component : values.gradient(0, point)) CHECK(component == Catch::Approx(0.0).margin(2.0e-13));
|
||||
}
|
||||
|
||||
TEST_CASE("TOP samples the reconstructed field between its density nodes", "[serialize][top_export][mfem][reconstruction]") {
|
||||
namespace model = serif::model;
|
||||
namespace deformation = serif::geometry::deformation;
|
||||
namespace discretization = serif::discretization;
|
||||
namespace backend = discretization::backend::mfem;
|
||||
const auto star = fixture::make_model(4.0 / 3.0);
|
||||
const auto radial = seed::make_polytropic_seed(star);
|
||||
REQUIRE(radial);
|
||||
const auto finite_elements = model::make_stellar_discretization(star,
|
||||
{.radius = radial->scales.radius, .geometry_order = 6},
|
||||
deformation::DomainDeformation<deformation::SphericalHarmonicSurface>{
|
||||
.surface = {.maximum_degree = 0}, .stellar_interior = {}, .vacuum = {}},
|
||||
discretization::reconstruction::SurfacePower<fixture::Density>{.density_exponent = 4.0 / 3.0});
|
||||
const auto state = seed::backend::mfem::make_polytropic_state(star, finite_elements, *radial);
|
||||
REQUIRE(state);
|
||||
const auto& binding = finite_elements.template binding<fixture::Density>();
|
||||
const auto density = state->template get<fixture::Density>();
|
||||
::mfem::Vector coefficients(static_cast<int>(density.size()));
|
||||
std::ranges::copy(density, coefficients.GetData());
|
||||
backend::FieldValues<fixture::Density> values(binding, coefficients);
|
||||
::mfem::GridFunction polynomial(&binding.space());
|
||||
polynomial = binding.scatter(coefficients);
|
||||
const std::vector<double> displacement(finite_elements.deformation.reference.displacement_space.GetVSize(), 0.0);
|
||||
using Binding = std::remove_cvref_t<decltype(binding)>;
|
||||
auto temporary_binding = std::make_unique<Binding>(binding.space(), binding.reconstruction());
|
||||
serif::backend::mfem::serialize::detail::TopModelSampler sampler(*temporary_binding, density,
|
||||
finite_elements.deformation.reference, displacement, {0.0, 0.0, 1.0}, {0.0, 0.0, 0.0});
|
||||
temporary_binding.reset();
|
||||
int selected = -1;
|
||||
for (int element = 0; element < binding.mesh().GetNE(); ++element) {
|
||||
if (binding.reconstruction_element(element) != nullptr) { selected = element; break; }
|
||||
}
|
||||
REQUIRE(selected >= 0);
|
||||
::mfem::IntegrationPoint point;
|
||||
point.Set3(0.27, 0.41, 0.69);
|
||||
::mfem::Vector position(3);
|
||||
binding.mesh().GetElementTransformation(selected)->Transform(point, position);
|
||||
const double radius = position.Norml2();
|
||||
const double expected = values.value(selected, point);
|
||||
REQUIRE(std::abs(expected - polynomial.GetValue(selected, point)) > 1.0e-8 * radial->scales.central_density.value());
|
||||
const auto exported = sampler.density(radius, position[2] / radius, std::atan2(position[1], position[0]));
|
||||
INFO((exported ? "Sample succeeded" : exported.error().message));
|
||||
REQUIRE(exported);
|
||||
CHECK(*exported == Catch::Approx(expected).epsilon(2.0e-10));
|
||||
|
||||
// Rayfem receives a display approximation sampled from the same physical field.
|
||||
// This checks the parent/submesh coordinate correspondence without opening a viewer.
|
||||
serif::visualization::backend::mfem::StellarFrame frame(finite_elements);
|
||||
StationaryDisplayCache cache{.linearization = {state->template get<fixture::Shape>()},
|
||||
.displacement = ::mfem::Vector(finite_elements.template binding<fixture::Displacement>().size())};
|
||||
cache.displacement = 0.0;
|
||||
frame.update(*state, cache);
|
||||
const auto fields = frame.fields();
|
||||
REQUIRE(fields.front().name == "density (sampled)");
|
||||
const auto& display = *fields.front().gf;
|
||||
CHECK(display.FESpace()->GetMaxElementOrder() == binding.space().GetMaxElementOrder() + 3);
|
||||
const auto& parent_elements = static_cast<const ::mfem::SubMesh&>(frame.mesh()).GetParentElementIDMap();
|
||||
bool displayed = false;
|
||||
for (int element = 0; element < parent_elements.Size(); ++element) {
|
||||
if (parent_elements[element] != selected) continue;
|
||||
displayed = true;
|
||||
const auto& nodes = display.FESpace()->GetFE(element)->GetNodes();
|
||||
for (int node = 0; node < nodes.GetNPoints(); ++node) {
|
||||
const auto& at = nodes.IntPoint(node);
|
||||
CHECK(display.GetValue(element, at) == Catch::Approx(values.value(selected, at)).margin(3.0e-12 * radial->scales.central_density.value()));
|
||||
}
|
||||
}
|
||||
REQUIRE(displayed);
|
||||
}
|
||||
Reference in New Issue
Block a user