feat(mesh): non conforming vacuum
stroid can now generate non uniformly refined vacuum meshes. Note we still enforce that the stellar domain is fully conforming.
This commit is contained in:
486
tests/nonconformingTest.cpp
Normal file
486
tests/nonconformingTest.cpp
Normal file
@@ -0,0 +1,486 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "stroid/stroid.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
constexpr double kPi = 3.14159265358979323846;
|
||||
|
||||
stroid::config::MeshConfig Configuration(int order = 2, int stellar_level = 2,
|
||||
int bulk_level = 0, int outer_level = 0) {
|
||||
stroid::config::MeshConfig config;
|
||||
config.order = order;
|
||||
config.refinement_levels = stellar_level;
|
||||
config.vacuum_refinement_levels = bulk_level;
|
||||
config.vacuum_outer_refinement_levels = outer_level;
|
||||
config.optimization_methods = stroid::config::OptimizationMethods{false, true};
|
||||
return config;
|
||||
}
|
||||
|
||||
std::map<int, int> CountAttributes(const mfem::Mesh& mesh, bool boundary = false) {
|
||||
std::map<int, int> counts;
|
||||
for (int element = 0; element < (boundary ? mesh.GetNBE() : mesh.GetNE()); ++element) {
|
||||
++counts[boundary ? mesh.GetBdrAttribute(element) : mesh.GetAttribute(element)];
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
void ExpectConstrainedField(mfem::GridFunction& values) {
|
||||
mfem::Vector independent;
|
||||
values.GetTrueDofs(independent);
|
||||
mfem::GridFunction reconstructed(values.FESpace());
|
||||
reconstructed.SetFromTrueDofs(independent);
|
||||
reconstructed -= values;
|
||||
EXPECT_LT(reconstructed.Normlinf(), 5.0e-12);
|
||||
}
|
||||
|
||||
void ExpectGeometryAndCoordinateTraces(stroid::StroidMesh& generated, bool require_hanging_faces = true) {
|
||||
mfem::Mesh& mesh = *generated.mesh;
|
||||
ASSERT_NE(generated.exterior_coordinate, nullptr);
|
||||
mfem::GridFunction& coordinate = *generated.exterior_coordinate->values;
|
||||
ASSERT_EQ(coordinate.FESpace()->GetMesh(), &mesh);
|
||||
ExpectConstrainedField(*mesh.GetNodes());
|
||||
ExpectConstrainedField(coordinate);
|
||||
const int vacuum = static_cast<int>(generated.config.vacuum_id.value());
|
||||
int hanging_faces = 0;
|
||||
int stellar_faces = 0;
|
||||
double geometry_error = 0.0;
|
||||
double coordinate_error = 0.0;
|
||||
double stellar_trace_error = 0.0;
|
||||
mfem::Vector first(3), second(3);
|
||||
|
||||
for (int face = 0; face < mesh.GetNumFaces(); ++face) {
|
||||
const auto info = mesh.GetFaceInformation(face);
|
||||
if (!info.IsLocal()) continue;
|
||||
auto* transformation = mesh.GetFaceElementTransformations(face);
|
||||
ASSERT_NE(transformation->Elem1, nullptr);
|
||||
ASSERT_NE(transformation->Elem2, nullptr);
|
||||
const bool first_vacuum = transformation->Elem1->Attribute == vacuum;
|
||||
const bool second_vacuum = transformation->Elem2->Attribute == vacuum;
|
||||
const bool stellar_interface = first_vacuum != second_vacuum;
|
||||
if (info.IsNonconformingFine()) {
|
||||
++hanging_faces;
|
||||
EXPECT_EQ(first_vacuum, second_vacuum);
|
||||
}
|
||||
if (stellar_interface) {
|
||||
++stellar_faces;
|
||||
EXPECT_TRUE(info.IsConforming()) << "Stellar interface face " << face;
|
||||
}
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
mfem::IntegrationPoint point;
|
||||
point.Set2(i / 3.0, j / 3.0);
|
||||
transformation->SetAllIntPoints(&point);
|
||||
const auto& first_point = transformation->Elem1->GetIntPoint();
|
||||
const auto& second_point = transformation->Elem2->GetIntPoint();
|
||||
transformation->Elem1->Transform(first_point, first);
|
||||
transformation->Elem2->Transform(second_point, second);
|
||||
first -= second;
|
||||
geometry_error = std::max(geometry_error, first.Norml2());
|
||||
const double first_value = coordinate.GetValue(transformation->Elem1No, first_point);
|
||||
const double second_value = coordinate.GetValue(transformation->Elem2No, second_point);
|
||||
coordinate_error = std::max(coordinate_error, std::abs(first_value - second_value));
|
||||
if (stellar_interface) {
|
||||
stellar_trace_error = std::max({stellar_trace_error, std::abs(first_value), std::abs(second_value)});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (require_hanging_faces) EXPECT_GT(hanging_faces, 0);
|
||||
EXPECT_GT(stellar_faces, 0);
|
||||
EXPECT_LT(geometry_error, 5.0e-12);
|
||||
EXPECT_LT(coordinate_error, 5.0e-12);
|
||||
EXPECT_LT(stellar_trace_error, 5.0e-12);
|
||||
|
||||
int outer_faces = 0;
|
||||
double outer_trace_error = 0.0;
|
||||
for (int boundary = 0; boundary < mesh.GetNBE(); ++boundary) {
|
||||
if (mesh.GetBdrAttribute(boundary) != static_cast<int>(generated.config.inf_bdr_id.value())) continue;
|
||||
++outer_faces;
|
||||
auto* transformation = mesh.GetBdrFaceTransformations(boundary);
|
||||
const auto& quadrature = mfem::IntRules.Get(mfem::Geometry::SQUARE, 6);
|
||||
for (int q = 0; q < quadrature.GetNPoints(); ++q) {
|
||||
transformation->SetAllIntPoints(&quadrature.IntPoint(q));
|
||||
outer_trace_error = std::max(outer_trace_error,
|
||||
std::abs(coordinate.GetValue(transformation->Elem1No, transformation->Elem1->GetIntPoint()) - 1.0));
|
||||
}
|
||||
}
|
||||
EXPECT_GT(outer_faces, 0);
|
||||
EXPECT_LT(outer_trace_error, 5.0e-12);
|
||||
|
||||
double minimum = 1.0;
|
||||
double maximum = 0.0;
|
||||
double interior_error = 0.0;
|
||||
for (int element = 0; element < mesh.GetNE(); ++element) {
|
||||
const auto& quadrature = mfem::IntRules.Get(mfem::Geometry::CUBE, 6);
|
||||
for (int q = 0; q < quadrature.GetNPoints(); ++q) {
|
||||
const double value = coordinate.GetValue(element, quadrature.IntPoint(q));
|
||||
ASSERT_TRUE(std::isfinite(value));
|
||||
if (mesh.GetAttribute(element) == vacuum) {
|
||||
minimum = std::min(minimum, value);
|
||||
maximum = std::max(maximum, value);
|
||||
} else {
|
||||
interior_error = std::max(interior_error, std::abs(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
EXPECT_GE(minimum, -5.0e-12);
|
||||
EXPECT_LE(maximum, 1.0 + 5.0e-12);
|
||||
EXPECT_LT(interior_error, 5.0e-12);
|
||||
}
|
||||
|
||||
void ExpectPositiveJacobians(mfem::Mesh& mesh, int excluded_attribute = -1) {
|
||||
double minimum = std::numeric_limits<double>::infinity();
|
||||
int minimum_element = -1;
|
||||
for (int element = 0; element < mesh.GetNE(); ++element) {
|
||||
if (mesh.GetAttribute(element) == excluded_attribute) continue;
|
||||
auto* transformation = mesh.GetElementTransformation(element);
|
||||
auto inspect = [&](const mfem::IntegrationPoint& point) {
|
||||
transformation->SetIntPoint(&point);
|
||||
const double determinant = transformation->Jacobian().Det();
|
||||
ASSERT_TRUE(std::isfinite(determinant));
|
||||
if (determinant < minimum) {
|
||||
minimum = determinant;
|
||||
minimum_element = element;
|
||||
}
|
||||
};
|
||||
const auto& quadrature = mfem::IntRules.Get(mfem::Geometry::CUBE, 2 * transformation->Order() + 2);
|
||||
for (int q = 0; q < quadrature.GetNPoints(); ++q) inspect(quadrature.IntPoint(q));
|
||||
for (int i = 0; i <= 2; ++i) {
|
||||
for (int j = 0; j <= 2; ++j) {
|
||||
for (int k = 0; k <= 2; ++k) {
|
||||
mfem::IntegrationPoint point;
|
||||
point.Set3(i / 2.0, j / 2.0, k / 2.0);
|
||||
inspect(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EXPECT_GT(minimum, 0.0) << "Element " << minimum_element;
|
||||
}
|
||||
|
||||
double StellarVolume(stroid::StroidMesh& generated) {
|
||||
double volume = 0.0;
|
||||
for (int element = 0; element < generated.mesh->GetNE(); ++element) {
|
||||
if (generated.mesh->GetAttribute(element) == static_cast<int>(generated.config.vacuum_id.value())) continue;
|
||||
auto* transformation = generated.mesh->GetElementTransformation(element);
|
||||
const auto& quadrature = mfem::IntRules.Get(mfem::Geometry::CUBE, 3 * transformation->Order() + 3);
|
||||
for (int q = 0; q < quadrature.GetNPoints(); ++q) {
|
||||
const auto& point = quadrature.IntPoint(q);
|
||||
transformation->SetIntPoint(&point);
|
||||
volume += point.weight * transformation->Jacobian().Det();
|
||||
}
|
||||
}
|
||||
return volume;
|
||||
}
|
||||
|
||||
double SurfaceRadiusError(stroid::StroidMesh& generated) {
|
||||
double error = 0.0;
|
||||
mfem::Vector physical(3);
|
||||
for (int boundary = 0; boundary < generated.mesh->GetNBE(); ++boundary) {
|
||||
if (generated.mesh->GetBdrAttribute(boundary) != static_cast<int>(generated.config.surface_bdr_id.value())) continue;
|
||||
auto* transformation = generated.mesh->GetBdrElementTransformation(boundary);
|
||||
const auto& quadrature = mfem::IntRules.Get(mfem::Geometry::SQUARE, 8);
|
||||
for (int q = 0; q < quadrature.GetNPoints(); ++q) {
|
||||
transformation->Transform(quadrature.IntPoint(q), physical);
|
||||
physical(2) /= 1.0 - generated.config.flattening.value();
|
||||
error = std::max(error, std::abs(physical.Norml2() - generated.config.r_star.value()));
|
||||
}
|
||||
}
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(NonconformingMesh, UnspecifiedVacuumLevelsPreserveUniformGeneration) {
|
||||
auto config = Configuration(2, 1);
|
||||
config.vacuum_refinement_levels.reset();
|
||||
config.vacuum_outer_refinement_levels.reset();
|
||||
auto generated = stroid::GenerateMesh(config);
|
||||
EXPECT_TRUE(generated.mesh->Conforming());
|
||||
EXPECT_EQ(generated.mesh->GetNE(), 19 * 8);
|
||||
ExpectGeometryAndCoordinateTraces(generated, false);
|
||||
|
||||
config.vacuum_refinement_levels = 1;
|
||||
config.vacuum_outer_refinement_levels = 1;
|
||||
auto explicit_levels = stroid::GenerateMesh(config);
|
||||
EXPECT_EQ(explicit_levels.mesh->GetNE(), generated.mesh->GetNE());
|
||||
mfem::H1_FECollection collection(2, 3);
|
||||
mfem::FiniteElementSpace space(explicit_levels.mesh.get(), &collection);
|
||||
EXPECT_EQ(space.GetTrueVSize(), space.GetVSize());
|
||||
EXPECT_NEAR(StellarVolume(explicit_levels), StellarVolume(generated), 1.0e-12);
|
||||
}
|
||||
|
||||
TEST(NonconformingMesh, RejectsNegativeRefinementTargets) {
|
||||
for (int field = 0; field < 3; ++field) {
|
||||
auto config = Configuration();
|
||||
if (field == 0) config.refinement_levels = -1;
|
||||
if (field == 1) config.vacuum_refinement_levels = -1;
|
||||
if (field == 2) config.vacuum_outer_refinement_levels = -1;
|
||||
EXPECT_THROW(stroid::GenerateMesh(config), std::invalid_argument);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(NonconformingMesh, DefaultOuterLevelProtectsBothSurfacesAndSavesBulkDofs) {
|
||||
auto config = Configuration(2, 3);
|
||||
config.vacuum_outer_refinement_levels.reset();
|
||||
auto generated = stroid::GenerateMesh(config);
|
||||
ASSERT_NE(generated.reference_mesh->ncmesh, nullptr);
|
||||
const auto attributes = CountAttributes(*generated.mesh);
|
||||
EXPECT_EQ(attributes.at(1), 7 * 512);
|
||||
EXPECT_EQ(attributes.at(2), 6 * 512);
|
||||
EXPECT_LT(attributes.at(3), 6 * 512);
|
||||
const auto boundaries = CountAttributes(*generated.mesh, true);
|
||||
EXPECT_EQ(boundaries.at(1), 6 * 64);
|
||||
EXPECT_EQ(boundaries.at(2), 6 * 64);
|
||||
int vacuum_minimum = 3;
|
||||
int vacuum_maximum = 0;
|
||||
for (int element = 0; element < generated.mesh->GetNE(); ++element) {
|
||||
ASSERT_EQ(generated.mesh->GetAttribute(element), generated.reference_mesh->GetAttribute(element));
|
||||
if (generated.mesh->GetAttribute(element) == 3) {
|
||||
const int depth = generated.reference_mesh->ncmesh->GetElementDepth(element);
|
||||
vacuum_minimum = std::min(vacuum_minimum, depth);
|
||||
vacuum_maximum = std::max(vacuum_maximum, depth);
|
||||
}
|
||||
}
|
||||
EXPECT_LT(vacuum_minimum, 3);
|
||||
EXPECT_EQ(vacuum_maximum, 3);
|
||||
for (int face = 0; face < generated.reference_mesh->GetNumFaces(); ++face) {
|
||||
const auto information = generated.reference_mesh->GetFaceInformation(face);
|
||||
if (!information.IsLocal()) continue;
|
||||
const int first = generated.reference_mesh->ncmesh->GetElementDepth(information.element[0].index);
|
||||
const int second = generated.reference_mesh->ncmesh->GetElementDepth(information.element[1].index);
|
||||
EXPECT_LE(std::abs(first - second), 1);
|
||||
}
|
||||
mfem::H1_FECollection collection(2, 3);
|
||||
mfem::FiniteElementSpace reduced(generated.mesh.get(), &collection);
|
||||
EXPECT_LT(reduced.GetTrueVSize(), reduced.GetVSize());
|
||||
const auto stats = stroid::stats::ComputeMeshStats(generated,
|
||||
stroid::stats::MeshStatFeatures::REFINEMENT | stroid::stats::MeshStatFeatures::CONFORMITY |
|
||||
stroid::stats::MeshStatFeatures::ELEMENT_COUNT);
|
||||
ASSERT_TRUE(stats.refinement.has_value());
|
||||
ASSERT_TRUE(stats.conformity.has_value());
|
||||
ASSERT_TRUE(stats.element_counts.has_value());
|
||||
EXPECT_EQ(stats.refinement->vacuum.min_depth, vacuum_minimum);
|
||||
EXPECT_EQ(stats.refinement->vacuum.max_depth, vacuum_maximum);
|
||||
EXPECT_EQ(stats.refinement->core.min_depth, 3);
|
||||
EXPECT_EQ(stats.refinement->core.max_depth, 3);
|
||||
EXPECT_EQ(stats.refinement->geometry_dofs, reduced.GetVSize());
|
||||
EXPECT_EQ(stats.refinement->geometry_true_dofs, reduced.GetTrueVSize());
|
||||
EXPECT_TRUE(stats.conformity->hierarchy_enabled);
|
||||
EXPECT_FALSE(stats.conformity->conforming);
|
||||
EXPECT_GT(stats.conformity->n_nonconforming_faces, 0);
|
||||
EXPECT_EQ(stats.element_counts->vacuum, attributes.at(3));
|
||||
config.vacuum_refinement_levels.reset();
|
||||
auto uniform = stroid::GenerateMesh(config);
|
||||
mfem::FiniteElementSpace full(uniform.mesh.get(), &collection);
|
||||
EXPECT_LT(reduced.GetTrueVSize(), full.GetTrueVSize());
|
||||
EXPECT_NEAR(StellarVolume(generated), StellarVolume(uniform), 2.0e-12);
|
||||
EXPECT_NEAR(SurfaceRadiusError(generated), SurfaceRadiusError(uniform), 2.0e-13);
|
||||
ExpectGeometryAndCoordinateTraces(generated);
|
||||
ExpectPositiveJacobians(*generated.mesh);
|
||||
}
|
||||
|
||||
TEST(NonconformingMesh, OuterTargetCanExceedStellarTarget) {
|
||||
auto config = Configuration(2, 1, 0, 3);
|
||||
auto generated = stroid::GenerateMesh(config);
|
||||
EXPECT_EQ(CountAttributes(*generated.mesh, true).at(2), 6 * 64);
|
||||
ExpectGeometryAndCoordinateTraces(generated);
|
||||
ExpectPositiveJacobians(*generated.mesh);
|
||||
}
|
||||
|
||||
TEST(NonconformingMesh, InterfaceClosureRaisesCoarseStellarBoundaryToMatchVacuum) {
|
||||
auto generated = stroid::GenerateMesh(Configuration(2, 0, 0, 3));
|
||||
ASSERT_NE(generated.reference_mesh->ncmesh, nullptr);
|
||||
EXPECT_EQ(CountAttributes(*generated.mesh, true).at(2), 6 * 64);
|
||||
int envelope_maximum = 0;
|
||||
for (int element = 0; element < generated.reference_mesh->GetNE(); ++element) {
|
||||
if (generated.reference_mesh->GetAttribute(element) == static_cast<int>(generated.config.envelope_id.value())) {
|
||||
envelope_maximum = std::max(envelope_maximum, generated.reference_mesh->ncmesh->GetElementDepth(element));
|
||||
}
|
||||
}
|
||||
EXPECT_GT(envelope_maximum, 0);
|
||||
for (int face = 0; face < generated.reference_mesh->GetNumFaces(); ++face) {
|
||||
const auto information = generated.reference_mesh->GetFaceInformation(face);
|
||||
if (!information.IsLocal()) continue;
|
||||
const int first = generated.reference_mesh->ncmesh->GetElementDepth(information.element[0].index);
|
||||
const int second = generated.reference_mesh->ncmesh->GetElementDepth(information.element[1].index);
|
||||
EXPECT_LE(std::abs(first - second), 1);
|
||||
}
|
||||
ExpectGeometryAndCoordinateTraces(generated);
|
||||
ExpectPositiveJacobians(*generated.mesh);
|
||||
}
|
||||
|
||||
TEST(NonconformingMesh, RefinementAndExteriorCoordinateAreInvariantUnderSmallLengthScales) {
|
||||
auto config = Configuration(2, 1, 0, 3);
|
||||
auto reference = stroid::GenerateMesh(config);
|
||||
constexpr double scale = 1.0e-15;
|
||||
config.r_core = config.r_core.value() * scale;
|
||||
config.r_star = config.r_star.value() * scale;
|
||||
config.r_infinity = config.r_infinity.value() * scale;
|
||||
auto scaled = stroid::GenerateMesh(config);
|
||||
ASSERT_EQ(scaled.mesh->GetNE(), reference.mesh->GetNE());
|
||||
ASSERT_EQ(scaled.mesh->GetNodes()->Size(), reference.mesh->GetNodes()->Size());
|
||||
double coordinate_error = 0.0;
|
||||
for (int dof = 0; dof < scaled.mesh->GetNodes()->Size(); ++dof) {
|
||||
coordinate_error = std::max(coordinate_error,
|
||||
std::abs((*scaled.mesh->GetNodes())(dof) / scale - (*reference.mesh->GetNodes())(dof)));
|
||||
}
|
||||
EXPECT_LT(coordinate_error, 5.0e-12);
|
||||
ASSERT_EQ(scaled.exterior_coordinate->values->Size(), reference.exterior_coordinate->values->Size());
|
||||
mfem::Vector difference(*scaled.exterior_coordinate->values);
|
||||
difference -= *reference.exterior_coordinate->values;
|
||||
EXPECT_LT(difference.Normlinf(), 5.0e-12);
|
||||
ExpectGeometryAndCoordinateTraces(scaled);
|
||||
ExpectPositiveJacobians(*scaled.mesh);
|
||||
}
|
||||
|
||||
TEST(NonconformingMesh, CurvedGeometryAndScalarConstraintsAcrossOrdersAndMappings) {
|
||||
for (const std::string mapping : {"multi_block", "spherified"}) {
|
||||
for (const int order : {1, 2, 3, 4}) {
|
||||
SCOPED_TRACE(mapping + " order=" + std::to_string(order));
|
||||
auto config = Configuration(order);
|
||||
config.core_mapping = mapping;
|
||||
config.flattening = 0.2;
|
||||
config.core_id = 11;
|
||||
config.envelope_id = 17;
|
||||
config.vacuum_id = 23;
|
||||
config.surface_bdr_id = 31;
|
||||
config.inf_bdr_id = 37;
|
||||
auto generated = stroid::GenerateMesh(config);
|
||||
EXPECT_EQ(CountAttributes(*generated.mesh).size(), 3);
|
||||
EXPECT_EQ(CountAttributes(*generated.mesh, true).size(), 2);
|
||||
ExpectGeometryAndCoordinateTraces(generated);
|
||||
// The legacy spherified core has known corner degeneracies. Its
|
||||
// envelope and vacuum must still remain strictly oriented.
|
||||
ExpectPositiveJacobians(*generated.mesh, mapping == "spherified" ? 11 : -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(NonconformingMesh, NoVacuumLeavesOnlyTheUniformStellarMesh) {
|
||||
auto config = Configuration(2, 1, 0, 3);
|
||||
config.include_external_domain = false;
|
||||
EXPECT_THROW(stroid::GenerateMesh(config), std::invalid_argument);
|
||||
config.vacuum_refinement_levels.reset();
|
||||
config.vacuum_outer_refinement_levels.reset();
|
||||
auto generated = stroid::GenerateMesh(config);
|
||||
EXPECT_EQ(generated.mesh->GetNE(), 13 * 8);
|
||||
EXPECT_EQ(generated.exterior_coordinate, nullptr);
|
||||
EXPECT_EQ(CountAttributes(*generated.mesh).size(), 2);
|
||||
ExpectPositiveJacobians(*generated.mesh);
|
||||
}
|
||||
|
||||
TEST(NonconformingMesh, SerializationAndSubsequentRefinementPreserveHierarchy) {
|
||||
auto generated = stroid::GenerateMesh(Configuration());
|
||||
const auto path = std::filesystem::temp_directory_path() / "stroid_nonconforming_round_trip.smesh";
|
||||
stroid::IO::SaveStroidMesh(generated, path.string(), "Nonconforming hierarchy regression");
|
||||
auto result = stroid::IO::LoadStroidMesh(path.string());
|
||||
ASSERT_TRUE(result.has_value()) << result.error();
|
||||
auto loaded = std::move(*result);
|
||||
ASSERT_NE(loaded.reference_mesh->ncmesh, nullptr);
|
||||
ASSERT_NE(loaded.mesh->ncmesh, nullptr);
|
||||
EXPECT_EQ(loaded.config.vacuum_refinement_levels, generated.config.vacuum_refinement_levels);
|
||||
EXPECT_EQ(loaded.config.vacuum_outer_refinement_levels, generated.config.vacuum_outer_refinement_levels);
|
||||
ASSERT_EQ(loaded.mesh->GetNE(), generated.mesh->GetNE());
|
||||
for (int element = 0; element < loaded.mesh->GetNE(); ++element) {
|
||||
EXPECT_EQ(loaded.reference_mesh->ncmesh->GetElementDepth(element),
|
||||
generated.reference_mesh->ncmesh->GetElementDepth(element));
|
||||
}
|
||||
EXPECT_NEAR(StellarVolume(loaded), StellarVolume(generated), 2.0e-12);
|
||||
ExpectGeometryAndCoordinateTraces(loaded);
|
||||
stroid::refinement::UniformRefinement(loaded, 1);
|
||||
EXPECT_EQ(loaded.mesh->GetNE(), generated.mesh->GetNE() * 8);
|
||||
EXPECT_EQ(loaded.mesh->GetNE(), loaded.reference_mesh->GetNE());
|
||||
ExpectGeometryAndCoordinateTraces(loaded);
|
||||
ExpectPositiveJacobians(*loaded.mesh);
|
||||
std::error_code error;
|
||||
std::filesystem::remove(path, error);
|
||||
EXPECT_FALSE(error);
|
||||
}
|
||||
|
||||
TEST(NonconformingMesh, LinearPhysicalPatchSolveUsesIndependentDofs) {
|
||||
auto generated = stroid::GenerateMesh(Configuration());
|
||||
mfem::H1_FECollection collection(2, 3);
|
||||
mfem::FiniteElementSpace space(generated.mesh.get(), &collection);
|
||||
ASSERT_LT(space.GetTrueVSize(), space.GetVSize());
|
||||
mfem::FunctionCoefficient exact([](const mfem::Vector& point) {
|
||||
return 1.0 + 0.3 * point(0) - 0.2 * point(1) + 0.1 * point(2);
|
||||
});
|
||||
mfem::Array<int> boundary(generated.mesh->bdr_attributes.Max());
|
||||
boundary = 0;
|
||||
boundary[static_cast<int>(generated.config.inf_bdr_id.value()) - 1] = 1;
|
||||
mfem::Array<int> essential;
|
||||
space.GetEssentialTrueDofs(boundary, essential);
|
||||
mfem::GridFunction solution(&space);
|
||||
solution = 0.0;
|
||||
solution.ProjectBdrCoefficient(exact, boundary);
|
||||
mfem::LinearForm rhs(&space);
|
||||
rhs = 0.0;
|
||||
mfem::ConstantCoefficient one(1.0);
|
||||
mfem::BilinearForm form(&space);
|
||||
const auto& quadrature = mfem::IntRules.Get(mfem::Geometry::CUBE, 10);
|
||||
auto* diffusion = new mfem::DiffusionIntegrator(one);
|
||||
diffusion->SetIntRule(&quadrature);
|
||||
form.AddDomainIntegrator(diffusion);
|
||||
form.Assemble();
|
||||
mfem::OperatorPtr system;
|
||||
mfem::Vector independent, forcing;
|
||||
form.FormLinearSystem(essential, solution, rhs, system, independent, forcing);
|
||||
EXPECT_EQ(system->Height(), space.GetTrueVSize());
|
||||
mfem::GSSmoother preconditioner(static_cast<mfem::SparseMatrix&>(*system));
|
||||
mfem::CGSolver solver;
|
||||
solver.SetOperator(*system);
|
||||
solver.SetPreconditioner(preconditioner);
|
||||
solver.SetRelTol(1.0e-13);
|
||||
solver.SetAbsTol(1.0e-14);
|
||||
solver.SetMaxIter(1500);
|
||||
solver.SetPrintLevel(-1);
|
||||
solver.Mult(forcing, independent);
|
||||
ASSERT_TRUE(solver.GetConverged());
|
||||
mfem::Vector residual(forcing.Size());
|
||||
system->Mult(independent, residual);
|
||||
residual -= forcing;
|
||||
EXPECT_LT(residual.Norml2() / forcing.Norml2(), 2.0e-12);
|
||||
form.RecoverFEMSolution(independent, rhs, solution);
|
||||
EXPECT_LT(solution.ComputeL2Error(exact), 2.0e-8);
|
||||
ExpectConstrainedField(solution);
|
||||
}
|
||||
|
||||
TEST(NonconformingMesh, StellarVolumeAndSurfaceShapeConverge) {
|
||||
auto coarse_config = Configuration(2, 2);
|
||||
coarse_config.vacuum_outer_refinement_levels.reset();
|
||||
auto coarse = stroid::GenerateMesh(coarse_config);
|
||||
auto fine_config = coarse_config;
|
||||
fine_config.refinement_levels = 3;
|
||||
auto fine = stroid::GenerateMesh(fine_config);
|
||||
const double exact_volume = 4.0 * kPi / 3.0;
|
||||
const double coarse_volume_error = std::abs(StellarVolume(coarse) - exact_volume);
|
||||
const double fine_volume_error = std::abs(StellarVolume(fine) - exact_volume);
|
||||
EXPECT_GT(coarse_volume_error, 1.0e-10);
|
||||
EXPECT_LT(fine_volume_error, 0.5 * coarse_volume_error);
|
||||
EXPECT_LT(SurfaceRadiusError(fine), 0.5 * SurfaceRadiusError(coarse));
|
||||
}
|
||||
|
||||
TEST(NonconformingMesh, TMOPPreservesHangingConstraintsAndBoundaryTraces) {
|
||||
auto config = Configuration(1);
|
||||
auto initial = stroid::GenerateMesh(config);
|
||||
config.optimization_methods = stroid::config::OptimizationMethods{true, true};
|
||||
auto generated = stroid::GenerateMesh(config);
|
||||
mfem::Array<int> marker(generated.mesh->bdr_attributes.Max());
|
||||
marker = 1;
|
||||
mfem::Array<int> essential;
|
||||
generated.mesh->GetNodalFESpace()->GetEssentialTrueDofs(marker, essential);
|
||||
mfem::Vector initial_nodes, optimized_nodes;
|
||||
initial.mesh->GetNodes()->GetTrueDofs(initial_nodes);
|
||||
generated.mesh->GetNodes()->GetTrueDofs(optimized_nodes);
|
||||
ASSERT_EQ(initial_nodes.Size(), optimized_nodes.Size());
|
||||
for (const int dof : essential) EXPECT_NEAR(initial_nodes(dof), optimized_nodes(dof), 2.0e-13);
|
||||
ExpectGeometryAndCoordinateTraces(generated);
|
||||
ExpectPositiveJacobians(*generated.mesh);
|
||||
}
|
||||
Reference in New Issue
Block a user