feat(libmeanfield): centrifugal + pressure
This commit is contained in:
30
experiments/README.md
Normal file
30
experiments/README.md
Normal 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.
|
||||
211
experiments/experiment_main.cpp
Normal file
211
experiments/experiment_main.cpp
Normal 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();
|
||||
}
|
||||
65
experiments/experiment_results.cppm
Normal file
65
experiments/experiment_results.cppm
Normal 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)
|
||||
});
|
||||
}
|
||||
}
|
||||
616
experiments/gravity_accuracy_budget.cpp
Normal file
616
experiments/gravity_accuracy_budget.cpp
Normal 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
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user