feat(libmeanfield): centrifugal + pressure

This commit is contained in:
2026-08-04 14:24:55 -04:00
parent 9bc4f2758a
commit dc912fd15e
115 changed files with 260058 additions and 163261 deletions

30
experiments/README.md Normal file
View File

@@ -0,0 +1,30 @@
# Accuracy experiments
This directory is deliberately separate from `tests/`. It uses Catch2 only as
an MPI-aware runner, selector, configuration host, and failure reporter. A run
fails only when the calculation is invalid (for example, non-finite output or a
failed linear solve); measured accuracy is written as data, not as a regression
tolerance.
`experiment_main.cpp` registers the `experiment` Catch2 reporter. It collects
rows recorded with `record_experiment_result` and writes one wide CSV table on
MPI rank zero. Each row carries sweep parameters and numerical metrics, so
results can be plotted or compared across commits.
The initial budget has three sweeps at fixed mesh and polynomial orders:
1. linear-solver tolerance: `1e-8`, `1e-10`, `1e-12`, `1e-14`;
2. production quadrature boost: `0`, `4`, `8`;
3. reference-space decomposition: numerical field/potential error, analytic
projection error, and numerical-to-projection gap.
Run only the budget and choose its output path with:
```text
./mean_field_experiments --experiment-output gravity_budget.csv --catch2 "[accuracy]"
```
The executable needs the same dependencies, generated module mapping, and
configuration registration as the existing Catch2 test executable. Add
`experiment_main.cpp` and `gravity_accuracy_budget.cpp` as a second executable
next to that target; do not add them to the ordinary test executable.

View File

@@ -0,0 +1,211 @@
#include <catch2/catch_session.hpp>
#include <catch2/reporters/catch_reporter_registrars.hpp>
#include <catch2/reporters/catch_reporter_streaming_base.hpp>
#include <CLI/CLI.hpp>
#include <fourdst/config/config.h>
#include <mfem.hpp>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <set>
#include <sstream>
#include <string>
#include <string_view>
#include <vector>
#include <catch2/catch_test_case_info.hpp>
import mean_field;
import test_helpers;
import experiment;
using namespace experiment;
static std::string escape_csv(const std::string& value) {
if (value.find_first_of(",\"\n") == std::string::npos) {
return value;
}
std::string escaped{"\""};
for (const char character : value) {
if (character == '\"') {
escaped += "\"\"";
} else {
escaped += character;
}
}
escaped += '\"';
return escaped;
}
static void write_experiment_csv() {
int rank = 0;
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
if (rank != 0) {
return;
}
const std::vector<ExperimentResult> results = ExperimentRegistry::instance().results();
std::set<std::string> parameter_names;
std::set<std::string> metric_names;
for (const ExperimentResult& result : results) {
for (const auto& [name, value] : result.parameters) {
parameter_names.insert(name);
}
for (const auto& [name, value] : result.metrics) {
metric_names.insert(name);
}
}
const std::string output_path = ExperimentRegistry::instance().output_path();
std::ofstream output(output_path);
if (!output) {
std::cerr << "Unable to write experiment results to " << output_path << '\n';
return;
}
output << "experiment,case";
for (const std::string& name : parameter_names) {
output << ',' << escape_csv(name);
}
for (const std::string& name : metric_names) {
output << ',' << escape_csv(name);
}
output << '\n';
output << std::setprecision(17);
for (const ExperimentResult& result : results) {
output << escape_csv(result.experiment_name) << ',' << escape_csv(result.case_name);
for (const std::string& name : parameter_names) {
const auto iterator = result.parameters.find(name);
output << ',' << (iterator == result.parameters.end() ? "" : escape_csv(iterator->second));
}
for (const std::string& name : metric_names) {
const auto iterator = result.metrics.find(name);
output << ',';
if (iterator != result.metrics.end()) {
output << iterator->second;
}
}
output << '\n';
}
std::cout << "Wrote " << results.size() << " experiment rows to " << output_path << '\n';
}
class ExperimentReporter final : public Catch::StreamingReporterBase {
public:
using StreamingReporterBase::StreamingReporterBase;
static std::string getDescription() {
return "Compact console reporter that writes structured experiment measurements to CSV.";
}
void testCaseEnded(const Catch::TestCaseStats& statistics) override {
StreamingReporterBase::testCaseEnded(statistics);
const bool passed = statistics.totals.assertions.allPassed();
std::cout << (passed ? "PASS " : "FAIL ")
<< statistics.testInfo->name
<< " (" << statistics.totals.assertions.passed
<< " assertions)\n";
}
void testRunEnded(const Catch::TestRunStats& statistics) override {
StreamingReporterBase::testRunEnded(statistics);
write_experiment_csv();
}
};
CATCH_REGISTER_REPORTER("experiment", ExperimentReporter)
int main(int argc, char* argv[]) {
fourdst::config::Config<mean_field::utils::Args> config;
CLI::App app{"Mean Field accuracy experiments"};
app.allow_extras();
app.set_help_flag("--config-help", "Show mean-field configuration options");
fourdst::config::register_as_cli(config, app);
std::string output_path{"accuracy_budget.csv"};
app.add_option("--experiment-output", output_path, "CSV path for structured measurements");
std::vector<std::string> configuration_arguments{argv[0]};
std::vector<std::string> catch_arguments_from_command_line;
bool parsing_catch_arguments = false;
for (int index = 1; index < argc; ++index) {
if (std::string_view(argv[index]) == "--catch2") {
parsing_catch_arguments = true;
continue;
}
if (parsing_catch_arguments) {
catch_arguments_from_command_line.emplace_back(argv[index]);
} else {
configuration_arguments.emplace_back(argv[index]);
}
}
std::vector<const char*> configuration_argv;
configuration_argv.reserve(configuration_arguments.size());
for (const std::string& argument : configuration_arguments) {
configuration_argv.push_back(argument.c_str());
}
try {
app.parse(static_cast<int>(configuration_argv.size()), configuration_argv.data());
} catch (const CLI::ParseError& error) {
return app.exit(error);
}
std::vector<std::string> catch_arguments{argv[0]};
for (const std::string& argument : app.remaining()) {
catch_arguments.push_back(argument);
}
for (const std::string& argument : catch_arguments_from_command_line) {
catch_arguments.push_back(argument);
}
bool has_reporter = false;
for (const std::string& argument : catch_arguments) {
has_reporter = has_reporter || argument == "-r" || argument == "--reporter" ||
argument.starts_with("-r=") || argument.starts_with("--reporter=");
}
if (!has_reporter) {
catch_arguments.emplace_back("--reporter");
catch_arguments.emplace_back("experiment");
}
std::vector<const char*> catch_argv;
catch_argv.reserve(catch_arguments.size());
for (const std::string& argument : catch_arguments) {
catch_argv.push_back(argument.c_str());
}
Catch::Session session;
if (const int parse_result = session.applyCommandLine(static_cast<int>(catch_argv.size()), catch_argv.data());
parse_result != 0) {
return parse_result;
}
mfem::Mpi::Init(argc, argv);
mfem::Device device("cpu");
mean_field::utils::Args args = config.main();
if (app.count("--mesh_file") == 0) {
args.mesh_file = "sandbox.smesh";
}
if (app.count("--p.rtol") == 0) {
args.p.rtol = 1.0e-12;
}
if (app.count("--p.atol") == 0) {
args.p.atol = 1.0e-12;
}
ExperimentRegistry::instance().set_output_path(output_path);
test_utils::set_args(std::move(args));
return session.run();
}

View File

@@ -0,0 +1,65 @@
module;
#include <algorithm>
#include <map>
#include <mutex>
#include <set>
#include <string>
#include <utility>
#include <vector>
export module experiment;
export namespace experiment {
struct ExperimentResult {
std::string experiment_name;
std::string case_name;
std::map<std::string, std::string> parameters;
std::map<std::string, double> metrics;
};
class ExperimentRegistry {
public:
static ExperimentRegistry& instance() {
static ExperimentRegistry registry;
return registry;
}
void add_result(ExperimentResult result) {
std::scoped_lock lock(m_mutex);
m_results.push_back(std::move(result));
}
[[nodiscard]] std::vector<ExperimentResult> results() const {
std::scoped_lock lock(m_mutex);
return m_results;
}
void set_output_path(std::string output_path) {
std::scoped_lock lock(m_mutex);
m_output_path = std::move(output_path);
}
[[nodiscard]] std::string output_path() const {
std::scoped_lock lock(m_mutex);
return m_output_path;
}
private:
mutable std::mutex m_mutex;
std::vector<ExperimentResult> m_results;
std::string m_output_path{"accuracy_budget.csv"};
};
inline void record_experiment_result(
const std::string& experiment_name,
const std::string& case_name,
std::map<std::string, std::string> parameters,
std::map<std::string, double> metrics
) {
ExperimentRegistry::instance().add_result({
.experiment_name = experiment_name,
.case_name = case_name,
.parameters = std::move(parameters),
.metrics = std::move(metrics)
});
}
}

View File

@@ -0,0 +1,616 @@
#include <catch2/catch_test_macros.hpp>
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <map>
#include <string>
#include <mfem.hpp>
#include <mpi.h>
import mean_field;
import test_helpers;
import experiment;
using namespace experiment;
struct AccuracyBudgetEnergies {
double binding{0.0};
double virial{0.0};
};
struct AccuracyBudgetMetrics {
double direct_relative_residual{0.0};
double gradient_relative_error{0.0};
double gradient_projection_relative_error{0.0};
double gradient_solution_projection_gap{0.0};
double potential_relative_error{0.0};
double potential_projection_relative_error{0.0};
double potential_solution_projection_gap{0.0};
double binding_relative_error{0.0};
double virial_relative_error{0.0};
double virial_consistency_error{0.0};
};
static double global_norm(const mfem::Vector& vector, MPI_Comm communicator) {
const double local_norm_squared = vector * vector;
double global_norm_squared = 0.0;
MPI_Allreduce(&local_norm_squared, &global_norm_squared, 1, MPI_DOUBLE, MPI_SUM, communicator);
return std::sqrt(global_norm_squared);
}
static double global_dot(const mfem::Vector& left, const mfem::Vector& right, MPI_Comm communicator) {
const double local_dot = left * right;
double global_dot_product = 0.0;
MPI_Allreduce(&local_dot, &global_dot_product, 1, MPI_DOUBLE, MPI_SUM, communicator);
return global_dot_product;
}
static void zero_vacuum_density(const mean_field::fem::FEM& fem, mfem::GridFunction& density) {
for (int index = 0; index < fem.vacuum_tdof_rho.Size(); ++index) {
density(fem.vacuum_tdof_rho[index]) = 0.0;
}
}
static int diagnostic_quadrature_order(const mean_field::fem::FEM& fem) {
return 2 * std::max(fem.L2_fes->GetMaxElementOrder(), fem.RT_fes->GetMaxElementOrder()) + 8;
}
static mfem::Vector assemble_monopole_projection_rhs(
mean_field::fem::FEM& fem,
const mfem::GridFunction& displacement,
const double mass,
const double stellar_radius
) {
static_cast<void>(displacement);
mfem::Vector local_rhs(fem.RT_fes->GetVSize());
local_rhs = 0.0;
const int vacuum_attribute = fem.domain_mapper_stateless->GetVacuumElementAttribute();
const int quadrature_order = diagnostic_quadrature_order(fem);
for (int element_id = 0; element_id < fem.mesh->GetNE(); ++element_id) {
const mfem::FiniteElement& gravity_element = *fem.RT_fes->GetFE(element_id);
mfem::ElementTransformation* transformation = fem.mesh->GetElementTransformation(element_id);
mfem::Array<int> gravity_dofs;
mfem::DofTransformation* gravity_transform = fem.RT_fes->GetElementVDofs(element_id, gravity_dofs);
const int dof_count = gravity_element.GetDof();
const int dimension = transformation->GetSpaceDim();
mfem::Vector element_rhs(dof_count);
mfem::Vector physical_position(dimension);
mfem::Vector analytic_field(dimension);
mfem::Vector pulled_field(dimension);
mfem::DenseMatrix mapping_jacobian(dimension);
mfem::DenseMatrix vector_shape(dof_count, dimension);
element_rhs = 0.0;
const mfem::IntegrationRule& rule = mfem::IntRules.Get(
transformation->GetGeometryType(),
quadrature_order
);
for (int quadrature_point_id = 0; quadrature_point_id < rule.GetNPoints(); ++quadrature_point_id) {
const mfem::IntegrationPoint& point = rule.IntPoint(quadrature_point_id);
fem.mapping->GetPhysicalPoint(*transformation, point, physical_position);
const double radius = physical_position.Norml2();
MFEM_VERIFY(std::isfinite(radius) && radius > 0.0, "Invalid radius in monopole projection RHS.");
analytic_field = physical_position;
if (transformation->Attribute == vacuum_attribute) {
analytic_field *= mean_field::utils::G * mass / (radius * radius * radius);
} else {
analytic_field *= mean_field::utils::G * mass /
(stellar_radius * stellar_radius * stellar_radius);
}
fem.mapping->ComputeJacobian(*transformation, mapping_jacobian);
mapping_jacobian.MultTranspose(analytic_field, pulled_field);
transformation->SetIntPoint(&point);
gravity_element.CalcVShape(*transformation, vector_shape);
const double reference_weight = point.weight * transformation->Weight();
for (int dof = 0; dof < dof_count; ++dof) {
for (int component = 0; component < dimension; ++component) {
element_rhs(dof) += reference_weight * vector_shape(dof, component) * pulled_field(component);
}
}
}
if (gravity_transform != nullptr) {
gravity_transform->TransformDual(element_rhs);
}
local_rhs.AddElementVector(gravity_dofs, element_rhs);
}
mfem::Vector true_rhs(fem.RT_fes->GetTrueVSize());
true_rhs = 0.0;
const mfem::Operator* prolongation = fem.RT_fes->GetProlongationMatrix();
if (prolongation != nullptr) {
prolongation->MultTranspose(local_rhs, true_rhs);
} else {
true_rhs = local_rhs;
}
return true_rhs;
}
static mfem::Vector project_monopole_gradient(
mean_field::fem::FEM& fem,
const mfem::GridFunction& displacement,
const double mass,
const double stellar_radius
) {
mfem::Vector displacement_true;
displacement.GetTrueDofs(displacement_true);
const mfem::Vector projection_rhs = assemble_monopole_projection_rhs(
fem,
displacement,
mass,
stellar_radius
);
mean_field::operators::PreparedMappedHDivMassOperator mass_operator(
fem,
*fem.domain_mapper_stateless
);
mass_operator.Prepare(displacement_true);
mfem::CGSolver solver(fem.RT_fes->GetComm());
solver.SetOperator(mass_operator);
solver.SetRelTol(1.0e-11);
solver.SetAbsTol(1.0e-13);
solver.SetMaxIter(4000);
solver.SetPrintLevel(0);
mfem::Vector projected_gradient(fem.RT_fes->GetTrueVSize());
projected_gradient = 0.0;
solver.Mult(projection_rhs, projected_gradient);
mfem::Vector residual;
mass_operator.Mult(projected_gradient, residual);
residual -= projection_rhs;
const double relative_residual = global_norm(residual, fem.RT_fes->GetComm()) /
std::max(global_norm(projection_rhs, fem.RT_fes->GetComm()), std::numeric_limits<double>::epsilon());
REQUIRE(std::isfinite(relative_residual));
REQUIRE(relative_residual < 1.0e-8);
return projected_gradient;
}
static double mapped_hdiv_relative_gap(
mean_field::fem::FEM& fem,
const mfem::GridFunction& displacement,
const mfem::Vector& calculated,
const mfem::Vector& reference
) {
mfem::Vector displacement_true;
displacement.GetTrueDofs(displacement_true);
mean_field::operators::PreparedMappedHDivMassOperator mass_operator(
fem,
*fem.domain_mapper_stateless
);
mass_operator.Prepare(displacement_true);
mfem::Vector difference(calculated);
difference -= reference;
mfem::Vector difference_action;
mfem::Vector reference_action;
mass_operator.Mult(difference, difference_action);
mass_operator.Mult(reference, reference_action);
const double difference_energy = global_dot(difference, difference_action, fem.RT_fes->GetComm());
const double reference_energy = global_dot(reference, reference_action, fem.RT_fes->GetComm());
MFEM_VERIFY(reference_energy > 0.0, "Projected monopole field has zero mapped H(div) norm.");
return std::sqrt(std::max(0.0, difference_energy) / reference_energy);
}
static AccuracyBudgetEnergies measure_stellar_energies(
mean_field::fem::FEM& fem,
const mfem::GridFunction& density,
const mean_field::physics::GravitySolution& solution
) {
const int vacuum_attribute = fem.domain_mapper_stateless->GetVacuumElementAttribute();
const int quadrature_order = diagnostic_quadrature_order(fem);
double local_binding = 0.0;
double local_virial = 0.0;
mfem::Vector physical_position(3);
mfem::Vector reference_field(3);
mfem::Vector physical_field(3);
mfem::DenseMatrix mapping_jacobian(3);
for (int element_id = 0; element_id < fem.mesh->GetNE(); ++element_id) {
mfem::ElementTransformation* transformation = fem.mesh->GetElementTransformation(element_id);
if (transformation->Attribute == vacuum_attribute) {
continue;
}
const mfem::IntegrationRule& rule = mfem::IntRules.Get(
transformation->GetGeometryType(),
quadrature_order
);
for (int quadrature_point_id = 0; quadrature_point_id < rule.GetNPoints(); ++quadrature_point_id) {
const mfem::IntegrationPoint& point = rule.IntPoint(quadrature_point_id);
transformation->SetIntPoint(&point);
fem.mapping->GetPhysicalPoint(*transformation, point, physical_position);
fem.mapping->ComputeJacobian(*transformation, mapping_jacobian);
const double mapping_determinant = mapping_jacobian.Det();
MFEM_VERIFY(mapping_determinant > 0.0, "Non-positive mapping determinant in energy diagnostic.");
solution.gradPhi.GetVectorValue(element_id, point, reference_field);
mapping_jacobian.Mult(reference_field, physical_field);
physical_field /= mapping_determinant;
const double weight = point.weight * transformation->Weight() * mapping_determinant;
const double rho = density.GetValue(element_id, point);
const double phi = solution.phi.GetValue(element_id, point);
local_binding += 0.5 * rho * phi * weight;
local_virial -= rho * (physical_position * physical_field) * weight;
}
}
AccuracyBudgetEnergies energies;
MPI_Allreduce(&local_binding, &energies.binding, 1, MPI_DOUBLE, MPI_SUM, fem.L2_fes->GetComm());
MPI_Allreduce(&local_virial, &energies.virial, 1, MPI_DOUBLE, MPI_SUM, fem.L2_fes->GetComm());
return energies;
}
static double reduced_gravity_relative_residual(
mean_field::fem::FEM& fem,
const mfem::GridFunction& density,
const mfem::GridFunction& displacement,
const mean_field::physics::GravitySolution& solution
) {
using GravityFieldForm = mean_field::utils::blocks::gravity_field_form;
constexpr auto gradient_block = mean_field::utils::blocks::get_residual_block<GravityFieldForm>(
mean_field::utils::blocks::gravity_field.gradient_term
);
constexpr auto poisson_block = mean_field::utils::blocks::get_residual_block<GravityFieldForm>(
mean_field::utils::blocks::gravity_field.poisson_term
);
const std::array<int, GravityFieldForm::value_block_count> value_sizes{
fem.L2_fes->GetTrueVSize(), fem.Vec_H1_fes->GetTrueVSize(),
fem.RT_fes->GetTrueVSize(), fem.L2_fes->GetTrueVSize()
};
const std::array<int, GravityFieldForm::residual_block_count> residual_sizes{
fem.RT_fes->GetTrueVSize(), fem.L2_fes->GetTrueVSize()
};
const mean_field::utils::blocks::form_layout<GravityFieldForm> layout(value_sizes, residual_sizes);
mfem::Vector density_true;
mfem::Vector displacement_true;
mfem::Vector gradient_true;
mfem::Vector potential_true;
density.GetTrueDofs(density_true);
displacement.GetTrueDofs(displacement_true);
solution.gradPhi.GetTrueDofs(gradient_true);
solution.phi.GetTrueDofs(potential_true);
mean_field::operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(
fem,
*fem.domain_mapper_stateless
);
mean_field::operators::GravityFieldJacobianOperator jacobian(
fem,
*fem.domain_mapper_stateless,
linearization_context,
layout.value_offsets(),
layout.residual_offsets()
);
mean_field::operators::GravityFieldOperator field_operator(
fem,
*fem.domain_mapper_stateless,
linearization_context,
layout.value_offsets(),
jacobian
);
mean_field::operators::context::gravity_field::GravityFieldGeometryContext geometry_context(
fem,
*fem.domain_mapper_stateless
);
mean_field::operators::ReducedGravityFieldOperator reduced_operator(
field_operator,
geometry_context,
displacement_true
);
mfem::Vector right_hand_side;
reduced_operator.BuildRightHandSide(density_true, right_hand_side);
mfem::BlockVector state(layout.residual_offsets());
state = 0.0;
state.GetBlock(gradient_block) = gradient_true;
state.GetBlock(poisson_block) = potential_true;
mfem::Vector residual;
reduced_operator.Mult(state, residual);
residual -= right_hand_side;
return global_norm(residual, fem.L2_fes->GetComm()) /
std::max(global_norm(right_hand_side, fem.L2_fes->GetComm()), std::numeric_limits<double>::epsilon());
}
static AccuracyBudgetMetrics measure_monopole_accuracy(
mean_field::fem::FEM& fem,
const mfem::GridFunction& density,
const mfem::GridFunction& displacement,
const mean_field::physics::GravitySolution& solution,
const mfem::ParGridFunction& projected_potential,
const mfem::Vector& projected_gradient,
const double mass,
const double stellar_radius
) {
mfem::Vector solution_gradient;
solution.gradPhi.GetTrueDofs(solution_gradient);
mfem::Vector solution_potential;
mfem::Vector projection_potential;
solution.phi.GetTrueDofs(solution_potential);
projected_potential.GetTrueDofs(projection_potential);
mfem::ParGridFunction projected_gradient_grid_function(fem.RT_fes.get());
projected_gradient_grid_function.SetFromTrueDofs(projected_gradient);
double local_solution_gradient_error = 0.0;
double local_projection_gradient_error = 0.0;
double local_gradient_norm = 0.0;
double local_solution_potential_error = 0.0;
double local_projection_potential_error = 0.0;
double local_potential_norm = 0.0;
const int vacuum_attribute = fem.domain_mapper_stateless->GetVacuumElementAttribute();
const int quadrature_order = diagnostic_quadrature_order(fem);
mfem::Vector physical_position(3);
mfem::Vector analytic_gradient(3);
mfem::Vector solution_reference_gradient(3);
mfem::Vector projection_reference_gradient(3);
mfem::Vector solution_physical_gradient(3);
mfem::Vector projection_physical_gradient(3);
mfem::DenseMatrix mapping_jacobian(3);
for (int element_id = 0; element_id < fem.mesh->GetNE(); ++element_id) {
mfem::ElementTransformation* transformation = fem.mesh->GetElementTransformation(element_id);
const mfem::IntegrationRule& rule = mfem::IntRules.Get(
transformation->GetGeometryType(),
quadrature_order
);
for (int quadrature_point_id = 0; quadrature_point_id < rule.GetNPoints(); ++quadrature_point_id) {
const mfem::IntegrationPoint& point = rule.IntPoint(quadrature_point_id);
transformation->SetIntPoint(&point);
fem.mapping->GetPhysicalPoint(*transformation, point, physical_position);
fem.mapping->ComputeJacobian(*transformation, mapping_jacobian);
const double mapping_determinant = mapping_jacobian.Det();
MFEM_VERIFY(mapping_determinant > 0.0, "Non-positive mapping determinant in accuracy diagnostic.");
const double radius = physical_position.Norml2();
MFEM_VERIFY(std::isfinite(radius) && radius > 0.0, "Invalid radius in monopole diagnostic.");
analytic_gradient = physical_position;
double analytic_potential = 0.0;
if (transformation->Attribute == vacuum_attribute) {
analytic_gradient *= mean_field::utils::G * mass / (radius * radius * radius);
analytic_potential = -mean_field::utils::G * mass / radius;
} else {
analytic_gradient *= mean_field::utils::G * mass /
(stellar_radius * stellar_radius * stellar_radius);
analytic_potential = -mean_field::utils::G * mass *
(3.0 * stellar_radius * stellar_radius - radius * radius) /
(2.0 * stellar_radius * stellar_radius * stellar_radius);
}
solution.gradPhi.GetVectorValue(element_id, point, solution_reference_gradient);
mapping_jacobian.Mult(solution_reference_gradient, solution_physical_gradient);
solution_physical_gradient /= mapping_determinant;
projected_gradient_grid_function.GetVectorValue(element_id, point, projection_reference_gradient);
mapping_jacobian.Mult(projection_reference_gradient, projection_physical_gradient);
projection_physical_gradient /= mapping_determinant;
const double solution_potential_value = solution.phi.GetValue(element_id, point);
const double projection_potential_value = projected_potential.GetValue(element_id, point);
const double weight = point.weight * transformation->Weight() * mapping_determinant;
solution_physical_gradient -= analytic_gradient;
projection_physical_gradient -= analytic_gradient;
local_solution_gradient_error += weight * (solution_physical_gradient * solution_physical_gradient);
local_projection_gradient_error += weight * (projection_physical_gradient * projection_physical_gradient);
local_gradient_norm += weight * (analytic_gradient * analytic_gradient);
local_solution_potential_error += weight *
(solution_potential_value - analytic_potential) * (solution_potential_value - analytic_potential);
local_projection_potential_error += weight *
(projection_potential_value - analytic_potential) * (projection_potential_value - analytic_potential);
local_potential_norm += weight * analytic_potential * analytic_potential;
}
}
const std::array<double, 6> local_values{
local_solution_gradient_error, local_projection_gradient_error, local_gradient_norm,
local_solution_potential_error, local_projection_potential_error, local_potential_norm
};
std::array<double, 6> global_values{};
MPI_Allreduce(
local_values.data(), global_values.data(), static_cast<int>(local_values.size()),
MPI_DOUBLE, MPI_SUM, fem.L2_fes->GetComm()
);
const AccuracyBudgetEnergies energies = measure_stellar_energies(fem, density, solution);
const double analytic_energy = -3.0 * mean_field::utils::G * mass * mass / (5.0 * stellar_radius);
REQUIRE(global_values[2] > 0.0);
REQUIRE(global_values[5] > 0.0);
AccuracyBudgetMetrics metrics;
metrics.direct_relative_residual = reduced_gravity_relative_residual(fem, density, displacement, solution);
metrics.gradient_relative_error = std::sqrt(global_values[0] / global_values[2]);
metrics.gradient_projection_relative_error = std::sqrt(global_values[1] / global_values[2]);
metrics.gradient_solution_projection_gap = mapped_hdiv_relative_gap(
fem, displacement, solution_gradient, projected_gradient
);
metrics.potential_relative_error = std::sqrt(global_values[3] / global_values[5]);
metrics.potential_projection_relative_error = std::sqrt(global_values[4] / global_values[5]);
mfem::Vector potential_difference(solution_potential);
potential_difference -= projection_potential;
const double projection_potential_norm = global_norm(projection_potential, fem.L2_fes->GetComm());
REQUIRE(projection_potential_norm > 0.0);
metrics.potential_solution_projection_gap = global_norm(potential_difference, fem.L2_fes->GetComm()) /
projection_potential_norm;
metrics.binding_relative_error = std::abs(energies.binding - analytic_energy) / std::abs(analytic_energy);
metrics.virial_relative_error = std::abs(energies.virial - analytic_energy) / std::abs(analytic_energy);
metrics.virial_consistency_error = std::abs(energies.binding - energies.virial) /
std::max(std::abs(energies.binding), std::numeric_limits<double>::epsilon());
return metrics;
}
static void run_monopole_case(
const std::string& sweep_name,
const std::string& case_name,
mean_field::utils::Args args,
const double solver_tolerance,
const int quadrature_boost
) {
args.p.rtol = solver_tolerance;
args.p.atol = std::min(args.p.atol, solver_tolerance * 1.0e-2);
args.p.max_iters = std::max(args.p.max_iters, 2000);
args.quadrature.global_boost = quadrature_boost;
mean_field::fem::FEM fem = mean_field::fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(fem.mapping != nullptr);
REQUIRE(fem.domain_mapper_stateless != nullptr);
const double stellar_radius = mean_field::utils::RADIUS;
const double mass = mean_field::utils::MASS;
const double density_value = mass / ((4.0 / 3.0) * M_PI * stellar_radius * stellar_radius * stellar_radius);
mfem::ParGridFunction displacement(fem.Vec_H1_fes.get());
displacement = 0.0;
fem.mapping->ResetDisplacement();
mean_field::physics::update_stiffness_matrix(fem);
mfem::GridFunction density(fem.L2_fes.get());
density = density_value;
zero_vacuum_density(fem, density);
mean_field::analysis::conserve_mass(fem, density, mass);
fem.com = mean_field::analysis::get_com(fem, density);
fem.Q = mean_field::physics::compute_quadrupole_moment_tensor(fem, density, fem.com);
const mean_field::physics::GravitySolution solution =
mean_field::physics::grav_potential_new(fem, args, density, displacement);
auto analytic_potential = [mass, stellar_radius](const mfem::Vector& position) {
const double radius = position.Norml2();
if (radius >= stellar_radius) {
return -mean_field::utils::G * mass / radius;
}
return -mean_field::utils::G * mass *
(3.0 * stellar_radius * stellar_radius - radius * radius) /
(2.0 * stellar_radius * stellar_radius * stellar_radius);
};
mean_field::mapping::PhysicalPositionFunctionCoefficient potential_coefficient(
*fem.mapping,
analytic_potential
);
mfem::ParGridFunction projected_potential(fem.L2_fes.get());
projected_potential.ProjectCoefficient(potential_coefficient);
const mfem::Vector projected_gradient = project_monopole_gradient(
fem,
displacement,
mass,
stellar_radius
);
const AccuracyBudgetMetrics metrics = measure_monopole_accuracy(
fem,
density,
displacement,
solution,
projected_potential,
projected_gradient,
mass,
stellar_radius
);
REQUIRE(std::isfinite(metrics.direct_relative_residual));
REQUIRE(std::isfinite(metrics.gradient_relative_error));
REQUIRE(std::isfinite(metrics.potential_relative_error));
REQUIRE(std::isfinite(metrics.virial_consistency_error));
record_experiment_result(
sweep_name,
case_name,
{
{"solver_rtol", std::to_string(solver_tolerance)},
{"quadrature_global_boost", std::to_string(quadrature_boost)},
{"mesh_file", args.mesh_file}
},
{
{"direct_relative_residual", metrics.direct_relative_residual},
{"gradient_relative_error", metrics.gradient_relative_error},
{"gradient_projection_relative_error", metrics.gradient_projection_relative_error},
{"gradient_solution_projection_gap", metrics.gradient_solution_projection_gap},
{"potential_relative_error", metrics.potential_relative_error},
{"potential_projection_relative_error", metrics.potential_projection_relative_error},
{"potential_solution_projection_gap", metrics.potential_solution_projection_gap},
{"binding_relative_error", metrics.binding_relative_error},
{"virial_relative_error", metrics.virial_relative_error},
{"virial_consistency_error", metrics.virial_consistency_error}
}
);
}
TEST_CASE("Uniform Monopole Accuracy Budget: Solver Tolerance", tags::gravity & tags::accuracy & tags::integration) {
const mean_field::utils::Args args = test_utils::setup_args();
constexpr std::array<double, 4> solver_tolerances{1.0e-8, 1.0e-10, 1.0e-12, 1.0e-14};
for (const double solver_tolerance : solver_tolerances) {
run_monopole_case(
"solver_tolerance",
"uniform_monopole",
args,
solver_tolerance,
0
);
}
}
TEST_CASE("Uniform Monopole Accuracy Budget: Quadrature", tags::gravity & tags::accuracy & tags::integration) {
const mean_field::utils::Args args = test_utils::setup_args();
constexpr std::array<int, 3> quadrature_boosts{0, 4, 8};
for (const int quadrature_boost : quadrature_boosts) {
run_monopole_case(
"quadrature",
"uniform_monopole",
args,
1.0e-13,
quadrature_boost
);
}
}
TEST_CASE("Uniform Monopole Accuracy Budget: Projection Decomposition", tags::gravity & tags::accuracy & tags::integration) {
run_monopole_case(
"projection_decomposition",
"uniform_monopole",
test_utils::setup_args(),
1.0e-13,
0
);
}