feat(topology): vacuum coordinate and multi block
Two major changes in this version. First stroid now embeds a vacuum coordinate as part of its StroidMesh file (this is a packed set of mfem meshes and GridFunction). This is a logical coordinate from 0 at the stellar surface to 1 at the mesh surface / compactified infinity which can be used by consumers to much more stablly infer position in the vacuum region. Second, there is a new topology backend, multi_block, which has been made the default. See the readme for more information but the basic jist is that multi_block addes 6 transition blocks onto the edge of the core domain. This allows for a much more well conditioned transition from the internal cartesien region to the external spherical region. The mesh conditioning improves by roughly a factor of 1000 for the same refinement level when compared to the legacy topology. The legacy topology is maintained as a option if core_mapping is set to spherified in the config.
This commit is contained in:
348
tools/geometry_quality_experiment.cpp
Normal file
348
tools/geometry_quality_experiment.cpp
Normal file
@@ -0,0 +1,348 @@
|
||||
#include "stroid/stroid.h"
|
||||
#include "CLI/CLI.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
constexpr double infinity = std::numeric_limits<double>::infinity();
|
||||
|
||||
struct SampleLocation {
|
||||
int element = -1;
|
||||
mfem::IntegrationPoint point;
|
||||
std::string source = "none";
|
||||
};
|
||||
|
||||
struct ConditioningStats {
|
||||
int elements = 0;
|
||||
size_t samples = 0;
|
||||
size_t nonpositive_samples = 0;
|
||||
double min_det = infinity;
|
||||
double min_sigma = infinity;
|
||||
double max_condition = 0.0;
|
||||
double min_scaled_jacobian = infinity;
|
||||
double contraction_boundary = infinity;
|
||||
SampleLocation det_location;
|
||||
SampleLocation condition_location;
|
||||
SampleLocation contraction_location;
|
||||
};
|
||||
|
||||
class ScopedOutputRedirect {
|
||||
std::streambuf* original;
|
||||
public:
|
||||
ScopedOutputRedirect() : original(std::cout.rdbuf(std::cerr.rdbuf())) {}
|
||||
~ScopedOutputRedirect() { std::cout.rdbuf(original); }
|
||||
};
|
||||
|
||||
std::vector<mfem::IntegrationPoint> ClosedSamples(int grid_points) {
|
||||
std::vector<mfem::IntegrationPoint> points;
|
||||
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));
|
||||
points.push_back(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const double offset : {0.005, 0.010885670927, 0.02}) {
|
||||
for (int corner = 0; corner < 8; ++corner) {
|
||||
mfem::IntegrationPoint point;
|
||||
point.Set3((corner & 1) ? 1.0 - offset : offset,
|
||||
(corner & 2) ? 1.0 - offset : offset,
|
||||
(corner & 4) ? 1.0 - offset : offset);
|
||||
points.push_back(point);
|
||||
}
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
double ColumnNorm(const mfem::DenseMatrix& matrix, int column) {
|
||||
double norm_squared = 0.0;
|
||||
for (int row = 0; row < 3; ++row) {
|
||||
norm_squared += matrix(row, column) * matrix(row, column);
|
||||
}
|
||||
return std::sqrt(norm_squared);
|
||||
}
|
||||
|
||||
double ContractionBoundary(const mfem::DenseMatrix& jacobian,
|
||||
const mfem::DenseMatrix& direction) {
|
||||
std::array<double, 4> coefficients{};
|
||||
mfem::DenseMatrix mixed(3);
|
||||
for (int mask = 0; mask < 8; ++mask) {
|
||||
int degree = 0;
|
||||
for (int column = 0; column < 3; ++column) {
|
||||
const bool use_direction = (mask & (1 << column)) != 0;
|
||||
degree += use_direction;
|
||||
for (int row = 0; row < 3; ++row) {
|
||||
mixed(row, column) = use_direction ? direction(row, column) : jacobian(row, column);
|
||||
}
|
||||
}
|
||||
coefficients[degree] += mixed.Det();
|
||||
}
|
||||
if (!(coefficients[0] > 0.0)) return 0.0;
|
||||
|
||||
auto polynomial = [&](double x) {
|
||||
return ((coefficients[3] * x + coefficients[2]) * x + coefficients[1]) * x + coefficients[0];
|
||||
};
|
||||
std::vector<double> breaks{0.0, 1.0};
|
||||
auto add_break = [&](double x) {
|
||||
if (std::isfinite(x) && x > 0.0 && x < 1.0) breaks.push_back(x);
|
||||
};
|
||||
const double a = 3.0 * coefficients[3];
|
||||
const double b = 2.0 * coefficients[2];
|
||||
const double c = coefficients[1];
|
||||
if (a == 0.0) {
|
||||
if (b != 0.0) add_break(-c / b);
|
||||
} else {
|
||||
const double discriminant = b * b - 4.0 * a * c;
|
||||
if (discriminant >= 0.0) {
|
||||
const double q = -0.5 * (b + std::copysign(std::sqrt(discriminant), b));
|
||||
if (q == 0.0) {
|
||||
add_break(-b / (2.0 * a));
|
||||
} else {
|
||||
add_break(q / a);
|
||||
add_break(c / q);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::sort(breaks.begin(), breaks.end());
|
||||
const double scale = std::abs(coefficients[0]) + std::abs(coefficients[1])
|
||||
+ std::abs(coefficients[2]) + std::abs(coefficients[3]);
|
||||
const double tolerance = 64.0 * std::numeric_limits<double>::epsilon() * scale;
|
||||
for (size_t i = 1; i < breaks.size(); ++i) {
|
||||
double right = breaks[i];
|
||||
const double value = polynomial(right);
|
||||
if (value > tolerance) continue;
|
||||
if (std::abs(value) <= tolerance) return right;
|
||||
double left = breaks[i - 1];
|
||||
for (int iteration = 0; iteration < 64; ++iteration) {
|
||||
const double middle = 0.5 * (left + right);
|
||||
if (polynomial(middle) > 0.0) left = middle;
|
||||
else right = middle;
|
||||
}
|
||||
return right;
|
||||
}
|
||||
return infinity;
|
||||
}
|
||||
|
||||
std::unique_ptr<mfem::GridFunction> BuildContractionProbe(
|
||||
stroid::StroidMesh& mesh, mfem::FiniteElementSpace& space, double stellar_radius
|
||||
) {
|
||||
auto values = std::make_unique<mfem::GridFunction>(&space);
|
||||
*values = 0.0;
|
||||
std::vector<bool> processed(space.GetNDofs(), false);
|
||||
mfem::Array<int> dofs;
|
||||
mfem::Vector physical(3), logical(3);
|
||||
for (int element = 0; element < mesh.mesh->GetNE(); ++element) {
|
||||
const auto& nodes = space.GetFE(element)->GetNodes();
|
||||
space.GetElementDofs(element, dofs);
|
||||
auto* physical_transform = mesh.mesh->GetElementTransformation(element);
|
||||
auto* logical_transform = mesh.reference_mesh->GetElementTransformation(element);
|
||||
for (int local = 0; local < dofs.Size(); ++local) {
|
||||
const int dof = dofs[local] >= 0 ? dofs[local] : -1 - dofs[local];
|
||||
if (processed[dof]) continue;
|
||||
physical_transform->Transform(nodes.IntPoint(local), physical);
|
||||
logical_transform->Transform(nodes.IntPoint(local), logical);
|
||||
const double radius = physical.Norml2();
|
||||
const double logical_radius = std::max({std::abs(logical(0)), std::abs(logical(1)), std::abs(logical(2))});
|
||||
const double fraction = std::min(logical_radius / stellar_radius, 1.0);
|
||||
for (int component = 0; component < 3; ++component) {
|
||||
(*values)(space.DofToVDof(dof, component)) = radius > 0.0
|
||||
? -stellar_radius * fraction * fraction * physical(component) / radius : 0.0;
|
||||
}
|
||||
processed[dof] = true;
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
std::map<int, ConditioningStats> InspectMesh(stroid::StroidMesh& mesh, int order,
|
||||
int grid_points, bool contraction_probe, int probe_order) {
|
||||
std::map<int, ConditioningStats> result;
|
||||
const auto closed_samples = ClosedSamples(grid_points);
|
||||
mfem::H1_FECollection probe_collection(probe_order, 3);
|
||||
mfem::FiniteElementSpace probe_space(mesh.mesh.get(), &probe_collection, 3);
|
||||
std::unique_ptr<mfem::GridFunction> probe;
|
||||
if (contraction_probe) probe = BuildContractionProbe(mesh, probe_space, mesh.config.r_star.value());
|
||||
mfem::DenseMatrix probe_values, probe_shape, direction(3);
|
||||
mfem::Array<int> probe_dofs;
|
||||
|
||||
for (int element = 0; element < mesh.mesh->GetNE(); ++element) {
|
||||
const int attribute = mesh.mesh->GetAttribute(element);
|
||||
++result[attribute].elements;
|
||||
++result[0].elements;
|
||||
auto* transform = mesh.mesh->GetElementTransformation(element);
|
||||
const bool inspect_probe = contraction_probe && attribute != static_cast<int>(mesh.config.vacuum_id.value());
|
||||
if (inspect_probe) {
|
||||
probe_space.GetElementDofs(element, probe_dofs);
|
||||
probe_values.SetSize(3, probe_dofs.Size());
|
||||
probe_shape.SetSize(probe_dofs.Size(), 3);
|
||||
for (int local = 0; local < probe_dofs.Size(); ++local) {
|
||||
const int dof = probe_dofs[local] >= 0 ? probe_dofs[local] : -1 - probe_dofs[local];
|
||||
for (int component = 0; component < 3; ++component) {
|
||||
probe_values(component, local) = (*probe)(probe_space.DofToVDof(dof, component));
|
||||
}
|
||||
}
|
||||
}
|
||||
auto inspect_point = [&](const mfem::IntegrationPoint& point, const std::string& source) {
|
||||
transform->SetIntPoint(&point);
|
||||
const mfem::DenseMatrix& jacobian = transform->Jacobian();
|
||||
const double determinant = jacobian.Det();
|
||||
const double sigma_min = jacobian.CalcSingularvalue(2);
|
||||
const double sigma_max = jacobian.CalcSingularvalue(0);
|
||||
const double condition = sigma_min > 0.0 ? sigma_max / sigma_min : infinity;
|
||||
const double denominator = ColumnNorm(jacobian, 0) * ColumnNorm(jacobian, 1) * ColumnNorm(jacobian, 2);
|
||||
const double scaled_jacobian = denominator > 0.0 ? determinant / denominator : 0.0;
|
||||
double boundary = infinity;
|
||||
if (inspect_probe) {
|
||||
probe_space.GetFE(element)->CalcDShape(point, probe_shape);
|
||||
mfem::Mult(probe_values, probe_shape, direction);
|
||||
boundary = ContractionBoundary(jacobian, direction);
|
||||
}
|
||||
for (const int region : {0, attribute}) {
|
||||
auto& stats = result[region];
|
||||
++stats.samples;
|
||||
if (!(determinant > 0.0)) ++stats.nonpositive_samples;
|
||||
if (determinant < stats.min_det) {
|
||||
stats.min_det = determinant;
|
||||
stats.det_location = {element, point, source};
|
||||
}
|
||||
stats.min_sigma = std::min(stats.min_sigma, sigma_min);
|
||||
stats.min_scaled_jacobian = std::min(stats.min_scaled_jacobian, scaled_jacobian);
|
||||
if (condition > stats.max_condition) {
|
||||
stats.max_condition = condition;
|
||||
stats.condition_location = {element, point, source};
|
||||
}
|
||||
if (boundary < stats.contraction_boundary) {
|
||||
stats.contraction_boundary = boundary;
|
||||
stats.contraction_location = {element, point, source};
|
||||
}
|
||||
}
|
||||
};
|
||||
const auto& quadrature = mfem::IntRules.Get(transform->GetGeometryType(), 2 * order + 4);
|
||||
for (int point = 0; point < quadrature.GetNPoints(); ++point) {
|
||||
inspect_point(quadrature.IntPoint(point), "quadrature");
|
||||
}
|
||||
for (const auto& point : closed_samples) inspect_point(point, "closed_grid_and_corner_probes");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void WriteLocation(std::ostream& output, const SampleLocation& location, stroid::StroidMesh& mesh) {
|
||||
output << ',' << location.element << ',' << location.source;
|
||||
if (location.element < 0) {
|
||||
output << ",nan,nan,nan,nan,nan,nan,nan,nan,nan";
|
||||
return;
|
||||
}
|
||||
mfem::Vector physical(3), logical(3);
|
||||
mesh.mesh->GetElementTransformation(location.element)->Transform(location.point, physical);
|
||||
mesh.reference_mesh->GetElementTransformation(location.element)->Transform(location.point, logical);
|
||||
output << ',' << location.point.x << ',' << location.point.y << ',' << location.point.z;
|
||||
for (int component = 0; component < 3; ++component) output << ',' << physical(component);
|
||||
for (int component = 0; component < 3; ++component) output << ',' << logical(component);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
std::vector<int> orders{1, 2, 3, 4, 5, 6};
|
||||
std::vector<int> refinements{0, 1, 2};
|
||||
std::vector<std::string> mappings{"spherified", "multi_block"};
|
||||
std::string output_path;
|
||||
int grid_points = 5;
|
||||
int probe_order = 3;
|
||||
double core_radius = 0.25;
|
||||
double infinity_radius = 5.0;
|
||||
double flattening = 0.0;
|
||||
bool no_external = false;
|
||||
bool contraction_probe = false;
|
||||
CLI::App app{"Compare signed Jacobians and conditioning of the actual high-order STROID mesh; TMOP is disabled."};
|
||||
app.add_option("--orders", orders, "Geometry orders, comma separated")->delimiter(',')->check(CLI::Range(1, 8));
|
||||
app.add_option("--refinements", refinements, "Uniform refinement levels, comma separated")->delimiter(',')->check(CLI::Range(0, 3));
|
||||
app.add_option("--mappings", mappings, "Core mappings, comma separated")->delimiter(',')->check(CLI::IsMember({"spherified", "multi_block"}));
|
||||
app.add_option("--grid-points", grid_points, "Closed tensor grid points per coordinate, plus near-corner probes")->check(CLI::Range(2, 15));
|
||||
app.add_option("--core-radius", core_radius, "Core radius; stellar radius is one");
|
||||
app.add_option("--infinity-radius", infinity_radius, "Outer reference radius");
|
||||
app.add_option("--flattening", flattening, "Spheroidal flattening");
|
||||
app.add_option("--output", output_path, "New CSV output file; defaults to stdout");
|
||||
app.add_flag("--no-external", no_external, "Omit exterior domain");
|
||||
app.add_flag("--contraction-probe", contraction_probe, "Inspect an interpolated unit logical-radius-squared radial contraction in stellar elements");
|
||||
app.add_option("--probe-order", probe_order, "H1 displacement order for the optional contraction probe")->check(CLI::Range(1, 8));
|
||||
try {
|
||||
app.parse(argc, argv);
|
||||
} catch (const CLI::ParseError& error) {
|
||||
return app.exit(error);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!std::isfinite(core_radius) || core_radius <= 0.0 || core_radius >= 1.0
|
||||
|| !std::isfinite(infinity_radius) || infinity_radius <= 1.0
|
||||
|| !std::isfinite(flattening) || flattening < 0.0 || flattening >= 1.0) {
|
||||
throw std::invalid_argument("Require 0 < core-radius < 1 < infinity-radius and 0 <= flattening < 1.");
|
||||
}
|
||||
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 << "mapping,order,refinement,r_core,r_star,r_infinity,flattening,external,grid_points,quadrature_order,probe_order,attribute,elements,samples,nonpositive_samples,min_signed_det,min_sigma,max_condition,min_scaled_jacobian,contraction_boundary_up_to_one";
|
||||
for (const std::string prefix : {"det", "condition", "contraction"}) {
|
||||
output << ',' << prefix << "_element," << prefix << "_source," << prefix << "_xi," << prefix << "_eta," << prefix << "_zeta," << prefix << "_x," << prefix << "_y," << prefix << "_z," << prefix << "_logical_x," << prefix << "_logical_y," << prefix << "_logical_z";
|
||||
}
|
||||
output << '\n';
|
||||
std::cerr << "Sampling actual FE geometry, not the analytical map. Attribute 0 aggregates all regions.\n"
|
||||
"Signed determinants and scaled Jacobians retain orientation; inf boundary means no sampled root through alpha=1.\n"
|
||||
"The optional contraction probe is a diagnostic field, not a Newton correction or a production exterior extension.\n";
|
||||
for (const auto& mapping : mappings) {
|
||||
for (const int order : orders) {
|
||||
for (const int refinement : refinements) {
|
||||
stroid::config::MeshConfig config;
|
||||
config.core_mapping = mapping;
|
||||
config.order = order;
|
||||
config.refinement_levels = refinement;
|
||||
config.r_core = core_radius;
|
||||
config.r_star = 1.0;
|
||||
config.r_infinity = infinity_radius;
|
||||
config.flattening = flattening;
|
||||
config.include_external_domain = !no_external;
|
||||
config.optimization_methods = stroid::config::OptimizationMethods{false, true};
|
||||
std::cerr << "Inspecting " << mapping << ", order " << order << ", refinement " << refinement << '\n';
|
||||
stroid::StroidMesh mesh;
|
||||
{
|
||||
ScopedOutputRedirect redirect;
|
||||
mesh = stroid::GenerateMesh(config);
|
||||
}
|
||||
const auto regions = InspectMesh(mesh, order, grid_points, contraction_probe, probe_order);
|
||||
for (const auto& [attribute, stats] : regions) {
|
||||
output << mapping << ',' << order << ',' << refinement << ',' << core_radius << ",1," << infinity_radius << ',' << flattening << ',' << !no_external << ',' << grid_points << ',' << 2 * order + 4 << ',' << (contraction_probe ? probe_order : 0) << ',' << attribute << ',' << stats.elements << ',' << stats.samples << ',' << stats.nonpositive_samples << ',' << stats.min_det << ',' << stats.min_sigma << ',' << stats.max_condition << ',' << stats.min_scaled_jacobian << ',' << stats.contraction_boundary;
|
||||
WriteLocation(output, stats.det_location, mesh);
|
||||
WriteLocation(output, stats.condition_location, mesh);
|
||||
WriteLocation(output, stats.contraction_location, mesh);
|
||||
output << '\n';
|
||||
}
|
||||
output.flush();
|
||||
if (!output) throw std::runtime_error("Failed to write experiment output.");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << "Geometry quality experiment failed: " << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user