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:
2026-09-09 12:39:27 -04:00
parent 2347ae152f
commit a0421d5ddc
30 changed files with 1641 additions and 133 deletions

View File

@@ -0,0 +1,337 @@
#include "stroid/stroid.h"
#include "CLI/CLI.hpp"
#include <algorithm>
#include <cmath>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <limits>
#include <string>
#include <vector>
namespace {
constexpr double kPi = 3.14159265358979323846;
class ScopedOutputRedirect {
std::streambuf* original;
public:
ScopedOutputRedirect() : original(std::cout.rdbuf(std::cerr.rdbuf())) {}
~ScopedOutputRedirect() { std::cout.rdbuf(original); }
};
struct GeometryResults {
int stellar_elements = 0;
int vacuum_elements = 0;
int hanging_faces = 0;
size_t samples = 0;
size_t nonpositive_samples = 0;
double min_signed_det = std::numeric_limits<double>::infinity();
double max_condition = 0.0;
double max_face_mismatch = 0.0;
double max_coordinate_mismatch = 0.0;
double coordinate_constraint_residual = 0.0;
double stellar_volume = 0.0;
double stellar_volume_error = 0.0;
double surface_radius_error = 0.0;
double outer_radius_error = 0.0;
};
GeometryResults InspectGeometry(stroid::StroidMesh& generated, int grid_points) {
GeometryResults result;
auto& mesh = *generated.mesh;
const int vacuum = static_cast<int>(generated.config.vacuum_id.value());
const auto& coordinate = *generated.exterior_coordinate->values;
mfem::Vector independent;
coordinate.GetTrueDofs(independent);
mfem::GridFunction reconstructed(generated.exterior_coordinate->space.get());
reconstructed.SetFromTrueDofs(independent);
reconstructed -= coordinate;
result.coordinate_constraint_residual = reconstructed.Normlinf();
for (int element = 0; element < mesh.GetNE(); ++element) {
const bool is_vacuum = mesh.GetAttribute(element) == vacuum;
is_vacuum ? ++result.vacuum_elements : ++result.stellar_elements;
auto* transformation = mesh.GetElementTransformation(element);
auto inspect = [&](const mfem::IntegrationPoint& point) {
transformation->SetIntPoint(&point);
const auto& jacobian = transformation->Jacobian();
const double determinant = jacobian.Det();
const double smallest = jacobian.CalcSingularvalue(2);
const double largest = jacobian.CalcSingularvalue(0);
++result.samples;
if (!(determinant > 0.0) || !std::isfinite(determinant)) ++result.nonpositive_samples;
result.min_signed_det = std::min(result.min_signed_det, determinant);
result.max_condition = std::max(result.max_condition,
smallest > 0.0 ? largest / smallest : std::numeric_limits<double>::infinity());
};
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);
inspect(point);
if (!is_vacuum) result.stellar_volume += point.weight * transformation->Jacobian().Det();
}
for (int i = 0; i < grid_points; ++i) {
for (int j = 0; j < grid_points; ++j) {
for (int k = 0; k < grid_points; ++k) {
mfem::IntegrationPoint point;
point.Set3(static_cast<double>(i) / (grid_points - 1),
static_cast<double>(j) / (grid_points - 1),
static_cast<double>(k) / (grid_points - 1));
inspect(point);
}
}
}
}
const double stellar_radius = generated.config.r_star.value();
const double flattening = generated.config.flattening.value();
const double analytic_volume = 4.0 * kPi * std::pow(stellar_radius, 3) * (1.0 - flattening) / 3.0;
result.stellar_volume_error = std::abs(result.stellar_volume - analytic_volume);
mfem::Vector first(3), second(3);
for (int face = 0; face < mesh.GetNumFaces(); ++face) {
const auto information = mesh.GetFaceInformation(face);
if (!information.IsLocal()) continue;
if (information.IsNonconformingFine()) ++result.hanging_faces;
auto* transformation = mesh.GetFaceElementTransformations(face);
const bool stellar_interface = (transformation->Elem1->Attribute == vacuum) !=
(transformation->Elem2->Attribute == vacuum);
if (stellar_interface && !information.IsConforming()) {
throw std::runtime_error("The stellar-vacuum interface is nonconforming.");
}
for (int i = 0; i < grid_points; ++i) {
for (int j = 0; j < grid_points; ++j) {
mfem::IntegrationPoint point;
point.Set2(static_cast<double>(i) / (grid_points - 1),
static_cast<double>(j) / (grid_points - 1));
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;
result.max_face_mismatch = std::max(result.max_face_mismatch, first.Norml2());
const double difference = coordinate.GetValue(transformation->Elem1No, first_point) -
coordinate.GetValue(transformation->Elem2No, second_point);
result.max_coordinate_mismatch = std::max(result.max_coordinate_mismatch, std::abs(difference));
}
}
}
const auto& surface_quadrature = mfem::IntRules.Get(mfem::Geometry::SQUARE, 10);
for (int boundary = 0; boundary < mesh.GetNBE(); ++boundary) {
const bool surface = mesh.GetBdrAttribute(boundary) == static_cast<int>(generated.config.surface_bdr_id.value());
const bool outer = mesh.GetBdrAttribute(boundary) == static_cast<int>(generated.config.inf_bdr_id.value());
if (!surface && !outer) continue;
const double radius = surface ? stellar_radius : generated.config.r_infinity.value();
auto* transformation = mesh.GetBdrElementTransformation(boundary);
for (int q = 0; q < surface_quadrature.GetNPoints(); ++q) {
transformation->Transform(surface_quadrature.IntPoint(q), first);
first(2) /= 1.0 - flattening;
double& error = surface ? result.surface_radius_error : result.outer_radius_error;
error = std::max(error, std::abs(first.Norml2() - radius));
}
}
return result;
}
struct SolveResults {
int dofs = 0;
int true_dofs = 0;
int iterations = 0;
bool converged = false;
double relative_residual = 0.0;
double l2_error = 0.0;
double stellar_l2_error = 0.0;
double vacuum_l2_error = 0.0;
double h1_error = 0.0;
};
SolveResults SolveManufacturedProblem(stroid::StroidMesh& generated, int solution_order) {
auto& mesh = *generated.mesh;
const double outer_sixth_power = std::pow(generated.config.r_infinity.value(), 6);
mfem::FunctionCoefficient exact([outer_sixth_power](const mfem::Vector& point) {
const double radius_squared = point * point;
return std::exp(-radius_squared) + 0.1 * std::pow(radius_squared, 3) / outer_sixth_power;
});
mfem::VectorFunctionCoefficient gradient(3, [outer_sixth_power](const mfem::Vector& point, mfem::Vector& value) {
const double radius_squared = point * point;
value = point;
value *= -2.0 * std::exp(-radius_squared) + 0.6 * radius_squared * radius_squared / outer_sixth_power;
});
mfem::FunctionCoefficient forcing([outer_sixth_power](const mfem::Vector& point) {
const double radius_squared = point * point;
return (6.0 - 4.0 * radius_squared) * std::exp(-radius_squared)
- 4.2 * radius_squared * radius_squared / outer_sixth_power;
});
mfem::H1_FECollection collection(solution_order, 3);
mfem::FiniteElementSpace space(&mesh, &collection);
SolveResults result;
result.dofs = space.GetVSize();
result.true_dofs = space.GetTrueVSize();
mfem::Array<int> boundary(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);
const int quadrature_order = 2 * solution_order + 3 * generated.config.order.value() + 4;
const auto& quadrature = mfem::IntRules.Get(mfem::Geometry::CUBE, quadrature_order);
mfem::LinearForm rhs(&space);
auto* load = new mfem::DomainLFIntegrator(forcing);
load->SetIntRule(&quadrature);
rhs.AddDomainIntegrator(load);
rhs.Assemble();
mfem::ConstantCoefficient one(1.0);
mfem::BilinearForm form(&space);
auto* diffusion = new mfem::DiffusionIntegrator(one);
diffusion->SetIntRule(&quadrature);
form.AddDomainIntegrator(diffusion);
form.Assemble();
mfem::OperatorPtr system;
mfem::Vector independent, system_rhs;
form.FormLinearSystem(essential, solution, rhs, system, independent, system_rhs);
mfem::GSSmoother preconditioner(static_cast<mfem::SparseMatrix&>(*system));
mfem::CGSolver solver;
solver.SetOperator(*system);
solver.SetPreconditioner(preconditioner);
solver.SetRelTol(1.0e-11);
solver.SetAbsTol(1.0e-14);
solver.SetMaxIter(2000);
solver.SetPrintLevel(-1);
solver.Mult(system_rhs, independent);
result.converged = solver.GetConverged();
result.iterations = solver.GetNumIterations();
mfem::Vector residual(system_rhs.Size());
system->Mult(independent, residual);
residual -= system_rhs;
result.relative_residual = residual.Norml2() / system_rhs.Norml2();
form.RecoverFEMSolution(independent, rhs, solution);
const mfem::IntegrationRule* rules[mfem::Geometry::NumGeom]{};
rules[mfem::Geometry::CUBE] = &quadrature;
result.l2_error = solution.ComputeL2Error(exact, rules);
result.h1_error = solution.ComputeH1Error(&exact, &gradient, rules);
for (int element = 0; element < mesh.GetNE(); ++element) {
auto* transformation = mesh.GetElementTransformation(element);
double element_error = 0.0;
for (int q = 0; q < quadrature.GetNPoints(); ++q) {
const auto& point = quadrature.IntPoint(q);
transformation->SetIntPoint(&point);
const double difference = solution.GetValue(element, point) - exact.Eval(*transformation, point);
element_error += point.weight * transformation->Weight() * difference * difference;
}
if (mesh.GetAttribute(element) == static_cast<int>(generated.config.vacuum_id.value())) {
result.vacuum_l2_error += element_error;
} else {
result.stellar_l2_error += element_error;
}
}
result.stellar_l2_error = std::sqrt(result.stellar_l2_error);
result.vacuum_l2_error = std::sqrt(result.vacuum_l2_error);
return result;
}
}
int main(int argc, char* argv[]) {
std::vector<int> orders{1, 2};
std::vector<int> refinements{2, 3};
std::vector<int> bulk_levels{0};
int outer_level = -1;
int solution_order = 1;
int grid_points = 3;
double flattening = 0.0;
double infinity_radius = 6.0;
std::string output_path;
CLI::App app{"Compare uniform and graded vacuum meshes, geometry, and an H1 manufactured Poisson solution; TMOP is disabled."};
app.add_option("--orders", orders, "Geometry orders, comma separated")->delimiter(',')->check(CLI::Range(1, 6));
app.add_option("--refinements", refinements, "Stellar refinement levels, comma separated")->delimiter(',')->check(CLI::Range(0, 4));
app.add_option("--bulk-levels", bulk_levels, "Vacuum background target levels, comma separated")->delimiter(',')->check(CLI::Range(0, 4));
app.add_option("--outer-level", outer_level, "Outer vacuum target; -1 inherits stellar level")->check(CLI::Range(-1, 4));
app.add_option("--solution-order", solution_order, "H1 polynomial order for the manufactured solve")->check(CLI::Range(1, 4));
app.add_option("--grid-points", grid_points, "Closed tensor sample grid per coordinate, in addition to quadrature")->check(CLI::Range(2, 9));
app.add_option("--flattening", flattening, "Spheroidal flattening")->check(CLI::Range(0.0, 0.9));
app.add_option("--infinity-radius", infinity_radius, "Finite geometric outer radius; stellar radius is one");
app.add_option("--output", output_path, "New CSV output file; default stdout");
try {
app.parse(argc, argv);
} catch (const CLI::ParseError& error) {
return app.exit(error);
}
try {
if (!std::isfinite(infinity_radius) || infinity_radius <= 1.0) {
throw std::invalid_argument("Require a finite infinity-radius greater than one.");
}
std::ofstream file;
if (!output_path.empty()) {
if (std::filesystem::exists(output_path)) throw std::runtime_error("Refusing to overwrite existing output: " + output_path);
file.open(output_path);
if (!file) throw std::runtime_error("Could not open output: " + output_path);
}
std::ostream& output = output_path.empty() ? std::cout : file;
output << std::setprecision(17);
output << "policy,geometry_order,stellar_level,bulk_target,outer_target,solution_order,flattening,r_infinity,elements,stellar_elements,vacuum_elements,vacuum_min_depth,vacuum_max_depth,geometry_dofs,geometry_true_dofs,solution_dofs,solution_true_dofs,hanging_faces,samples,nonpositive_samples,min_signed_det,max_condition,max_face_mismatch,max_coordinate_mismatch,coordinate_constraint_residual,stellar_volume,stellar_volume_error,surface_radius_error,outer_radius_error,l2_error,stellar_l2_error,vacuum_l2_error,h1_error,solver_converged,solver_iterations,relative_residual\n";
std::cerr << "Evaluating actual FE geometry using signed Jacobians at quadrature points and a closed grid.\n"
"Manufactured problem: -Delta u=f, u=exp(-r^2)+0.1*r^6/R^6, exact Dirichlet data only at the outer boundary.\n"
"This finite-domain Poisson diagnostic measures discretization error; it does not implement compactified physics.\n";
bool verified = true;
for (const int order : orders) {
for (const int refinement : refinements) {
std::vector<int> cases{-1};
cases.insert(cases.end(), bulk_levels.begin(), bulk_levels.end());
for (const int bulk : cases) {
const bool uniform = bulk < 0;
const std::string policy = uniform ? "uniform" : "graded";
stroid::config::MeshConfig config;
config.order = order;
config.refinement_levels = refinement;
config.flattening = flattening;
config.r_infinity = infinity_radius;
config.optimization_methods = stroid::config::OptimizationMethods{false, true};
if (!uniform) {
config.vacuum_refinement_levels = bulk;
if (outer_level >= 0) config.vacuum_outer_refinement_levels = outer_level;
}
std::cerr << "Inspecting " << policy << ", geometry order " << order << ", stellar level " << refinement
<< ", bulk " << (uniform ? refinement : bulk) << '\n';
stroid::StroidMesh generated;
{
ScopedOutputRedirect redirect;
generated = stroid::GenerateMesh(config);
}
const auto geometry = InspectGeometry(generated, grid_points);
const auto stats = stroid::stats::ComputeMeshStats(generated, stroid::stats::MeshStatFeatures::REFINEMENT);
const auto solve = SolveManufacturedProblem(generated, solution_order);
verified = verified && geometry.nonpositive_samples == 0 && geometry.max_face_mismatch < 1.0e-10
&& geometry.max_coordinate_mismatch < 1.0e-11 && geometry.coordinate_constraint_residual < 1.0e-11
&& solve.converged && solve.relative_residual < 1.0e-9 && std::isfinite(solve.h1_error);
const int outer = uniform ? refinement : outer_level < 0 ? refinement : outer_level;
output << policy << ',' << order << ',' << refinement << ',' << (uniform ? refinement : bulk) << ',' << outer
<< ',' << solution_order << ',' << flattening << ',' << infinity_radius << ',' << generated.mesh->GetNE()
<< ',' << geometry.stellar_elements << ',' << geometry.vacuum_elements
<< ',' << stats.refinement->vacuum.min_depth << ',' << stats.refinement->vacuum.max_depth
<< ',' << stats.refinement->geometry_dofs << ',' << stats.refinement->geometry_true_dofs
<< ',' << solve.dofs << ',' << solve.true_dofs << ',' << geometry.hanging_faces
<< ',' << geometry.samples << ',' << geometry.nonpositive_samples << ',' << geometry.min_signed_det
<< ',' << geometry.max_condition << ',' << geometry.max_face_mismatch << ',' << geometry.max_coordinate_mismatch
<< ',' << geometry.coordinate_constraint_residual << ',' << geometry.stellar_volume << ',' << geometry.stellar_volume_error
<< ',' << geometry.surface_radius_error << ',' << geometry.outer_radius_error << ',' << solve.l2_error
<< ',' << solve.stellar_l2_error << ',' << solve.vacuum_l2_error << ',' << solve.h1_error
<< ',' << solve.converged << ',' << solve.iterations << ',' << solve.relative_residual << '\n';
output.flush();
if (!output) throw std::runtime_error("Failed to write experiment output.");
}
}
}
if (!verified) {
std::cerr << "Numerical verification failed: inspect Jacobian, trace, or solver columns.\n";
return 1;
}
} catch (const std::exception& error) {
std::cerr << "Vacuum refinement experiment failed: " << error.what() << '\n';
return 1;
}
return 0;
}