Files
MeanField/experiments/material_surface_preconditioning.cpp
2026-09-04 07:54:10 -04:00

994 lines
51 KiB
C++

#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <limits>
#include <map>
#include <numbers>
#include <ranges>
#include <string>
#include <utility>
#include <vector>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
#include <mpi.h>
import experiment;
import mean_field;
import test_helpers;
namespace {
using Clock = std::chrono::steady_clock;
namespace backend = mean_field::preconditioning::backend;
namespace preconditioning = mean_field::preconditioning;
namespace solver = mean_field::solver;
struct MaterialBlockMeasurements final {
double density{0.0};
double surface{0.0};
double enthalpy{0.0};
};
[[nodiscard]] const char *buildConfiguration() noexcept {
#ifdef NDEBUG
return "release";
#else
return "debug";
#endif
}
[[nodiscard]] double maximumRankSeconds(
const Clock::time_point start,
const MPI_Comm communicator
) {
const double localSeconds = std::chrono::duration<double>(Clock::now() - start).count();
double maximumSeconds = 0.0;
MPI_Allreduce(&localSeconds, &maximumSeconds, 1, MPI_DOUBLE, MPI_MAX, communicator);
return maximumSeconds;
}
[[nodiscard]] double globalNorm(
const mfem::Vector &vector,
const MPI_Comm communicator
) {
const double localSquaredNorm = vector * vector;
double globalSquaredNorm = 0.0;
MPI_Allreduce(&localSquaredNorm, &globalSquaredNorm, 1, MPI_DOUBLE, MPI_SUM, communicator);
return std::sqrt(std::max(globalSquaredNorm, 0.0));
}
void announce(
const MPI_Comm communicator,
const std::string &message
) {
int rank = 0;
MPI_Comm_rank(communicator, &rank);
if (rank == 0) {
std::cout << "[P9 material-surface] " << message << std::endl;
}
}
[[nodiscard]] mean_field::operators::StellarEquilibriumDependencies makeDependencies() {
return {
.discretization = {.identity = 10103, .revision = 1},
.density = {.identity = 10111, .revision = 1},
.surfaceDeformation = {.identity = 10133, .revision = 1},
.gravityGradient = {.identity = 10139, .revision = 1},
.gravityPotential = {.identity = 10141, .revision = 1},
.enthalpy = {.identity = 10151, .revision = 1},
.bernoulliConstant = {.identity = 10159, .revision = 1},
.rotation = {.identity = 10163, .revision = 1},
.targetMass = {.identity = 10169, .revision = 1}
};
}
[[nodiscard]] mean_field::physics::RigidRotation zeroRotation() {
mfem::Vector angularVelocity(3);
mfem::Vector center(3);
angularVelocity = 0.0;
center = 0.0;
return {angularVelocity, center};
}
[[nodiscard]] mfem::Vector blockBalancedDirection(
const mfem::Array<int> &offsets,
const double phase,
const MPI_Comm communicator
) {
mfem::Vector direction(offsets.Last());
direction = 0.0;
for (int block = 0; block < offsets.Size() - 1; ++block) {
mfem::Vector values(direction, offsets[block], offsets[block + 1] - offsets[block]);
for (int index = 0; index < values.Size(); ++index) {
const double ordinal = static_cast<double>(index + 1);
values(index) = std::sin(0.371 * ordinal + phase + static_cast<double>(block)) +
0.29 * std::cos(0.173 * ordinal - 0.5 * phase);
}
const double norm = globalNorm(values, communicator);
REQUIRE(norm > 0.0);
values /= norm;
values.SyncAliasMemory(direction);
}
return direction;
}
[[nodiscard]] MaterialBlockMeasurements blockNorms(
const mfem::Vector &vector,
const mfem::Array<int> &offsets,
const MPI_Comm communicator
) {
REQUIRE(offsets.Size() == 4);
const mfem::Vector density(const_cast<mfem::real_t *>(vector.GetData()) + offsets[0], offsets[1] - offsets[0]);
const mfem::Vector surface(const_cast<mfem::real_t *>(vector.GetData()) + offsets[1], offsets[2] - offsets[1]);
const mfem::Vector enthalpy(const_cast<mfem::real_t *>(vector.GetData()) + offsets[2], offsets[3] - offsets[2]);
return {
.density = globalNorm(density, communicator),
.surface = globalNorm(surface, communicator),
.enthalpy = globalNorm(enthalpy, communicator)
};
}
[[nodiscard]] MaterialBlockMeasurements relativeBlockNorms(
const mfem::Vector &numerator,
const mfem::Vector &denominator,
const mfem::Array<int> &offsets,
const MPI_Comm communicator
) {
const MaterialBlockMeasurements numeratorNorms = blockNorms(numerator, offsets, communicator);
const MaterialBlockMeasurements denominatorNorms = blockNorms(denominator, offsets, communicator);
constexpr double floor = 1.0e-300;
return {
.density = numeratorNorms.density / std::max(denominatorNorms.density, floor),
.surface = numeratorNorms.surface / std::max(denominatorNorms.surface, floor),
.enthalpy = numeratorNorms.enthalpy / std::max(denominatorNorms.enthalpy, floor)
};
}
[[nodiscard]] std::map<
std::string,
std::string>
commonParameters(
const std::string &candidate,
const std::string &measurement,
const int dimension
) {
return {
{"build_configuration", buildConfiguration()},
{"candidate", candidate},
{"equation_of_state", "Polytrope(n=1)"},
{"experiment_schema", "p9_material_surface_v1"},
{"factorization", candidate},
{"linearization_state", "projected_lane_emden"},
{"measurement", measurement},
{"mesh_file", test_utils::setup_args().mesh_file},
{"operator", "restricted_material_surface_jacobian"},
{"preconditioned_product", "A_material_surface M^-1"},
{"root_dimension", std::to_string(dimension)},
{"rotation", "zero"}
};
}
void recordSpectrum(
const std::string &candidate,
const solver::ArnoldiSpectralMeasurement &spectrum,
const int dimension,
const double setupSeconds
) {
experiment::record_experiment_result(
"material_surface_preconditioning_p9", candidate + "_arnoldi_summary",
commonParameters(candidate, "arnoldi_summary", dimension),
{{"setup_seconds_maximum_rank", setupSeconds},
{"requested_dimension", static_cast<double>(spectrum.requestedDimension)},
{"achieved_dimension", static_cast<double>(spectrum.achievedDimension)},
{"invariant_subspace_found", spectrum.invariantSubspaceFound ? 1.0 : 0.0},
{"operator_applications", static_cast<double>(spectrum.operatorApplications)},
{"measurement_seconds_maximum_rank", spectrum.measurementSecondsMaximumRank},
{"operator_application_seconds_maximum_rank", spectrum.operatorApplicationSecondsMaximumRank},
{"projected_condition_proxy", spectrum.projectedConditionProxy},
{"projected_largest_singular_value", spectrum.projectedLargestSingularValue},
{"projected_smallest_singular_value", spectrum.projectedSmallestSingularValue},
{"centroid_real_part", spectrum.centroidRealPart},
{"centroid_imaginary_part", spectrum.centroidImaginaryPart},
{"rms_distance_from_one", spectrum.rmsDistanceFromOne},
{"rms_cluster_radius", spectrum.rmsClusterRadius},
{"minimum_magnitude", spectrum.minimumMagnitude},
{"maximum_magnitude", spectrum.maximumMagnitude},
{"minimum_real_part", spectrum.minimumRealPart},
{"maximum_real_part", spectrum.maximumRealPart},
{"maximum_absolute_imaginary_part", spectrum.maximumAbsoluteImaginaryPart},
{"negative_real_part_count", static_cast<double>(spectrum.negativeRealPartCount)},
{"converged_ritz_value_count", static_cast<double>(spectrum.convergedRitzValueCount)},
{"conjugate_pair_defect", spectrum.conjugatePairDefect},
{"projected_departure_from_normality", spectrum.projectedDepartureFromNormality},
{"field_of_values_minimum_real_part", spectrum.projectedFieldOfValuesMinimumRealPart},
{"field_of_values_maximum_real_part", spectrum.projectedFieldOfValuesMaximumRealPart}}
);
std::vector<solver::RitzValueMeasurement> ordered = spectrum.ritzValues;
std::ranges::sort(ordered, [](const auto &left, const auto &right) {
if (left.realPart != right.realPart) {
return left.realPart < right.realPart;
}
return left.imaginaryPart < right.imaginaryPart;
});
for (std::size_t index = 0; index < ordered.size(); ++index) {
const auto &value = ordered[index];
experiment::record_experiment_result(
"material_surface_preconditioning_p9", candidate + "_ritz_" + std::to_string(index),
commonParameters(candidate, "ritz_value", dimension),
{{"ritz_index", static_cast<double>(index)},
{"real_part", value.realPart},
{"imaginary_part", value.imaginaryPart},
{"magnitude", value.magnitude},
{"distance_from_one", value.distanceFromOne},
{"residual_estimate", value.residualEstimate},
{"relative_residual_estimate", value.relativeResidualEstimate},
{"converged", value.converged ? 1.0 : 0.0}}
);
}
}
template <typename Preconditioner>
void measureCandidate(
const std::string &candidate,
Preconditioner &inversePreconditioner,
const double setupSeconds,
const mean_field::preconditioning::MaterialSurfaceJacobianOperator &operation,
const mfem::Vector &exactCorrection,
const mfem::Vector &rightHandSide,
const mfem::Vector &arnoldiDirection,
const MPI_Comm communicator,
std::map<
std::string,
double> preparationMetrics = {}
) {
constexpr int maximumIterations = 40;
constexpr int restartDimension = 20;
constexpr int arnoldiDimension = 16;
solver::InstrumentedOperator instrumentedOperation(operation);
solver::InstrumentedPreconditioner instrumentedPreconditioner(inversePreconditioner);
solver::ResidualHistoryMonitor monitor;
mfem::FGMRESSolver krylov(communicator);
krylov.SetPreconditioner(instrumentedPreconditioner);
krylov.SetOperator(instrumentedOperation);
krylov.SetMonitor(monitor);
krylov.SetRelTol(1.0e-8);
krylov.SetAbsTol(1.0e-12);
krylov.SetMaxIter(maximumIterations);
krylov.SetKDim(restartDimension);
krylov.SetPrintLevel(0);
mfem::Vector solution(operation.Width());
solution = 0.0;
announce(communicator, "solving manufactured system with " + candidate);
const Clock::time_point solveStart = Clock::now();
krylov.Mult(rightHandSide, solution);
const double solveSeconds = maximumRankSeconds(solveStart, communicator);
mfem::Vector trueResidual(operation.Height());
operation.Mult(solution, trueResidual);
trueResidual -= rightHandSide;
mfem::Vector solutionError(solution);
solutionError -= exactCorrection;
const double trueRelativeResidual =
globalNorm(trueResidual, communicator) /
std::max(globalNorm(rightHandSide, communicator), std::numeric_limits<double>::min());
const double relativeSolutionError =
globalNorm(solutionError, communicator) /
std::max(globalNorm(exactCorrection, communicator), std::numeric_limits<double>::min());
const MaterialBlockMeasurements relativeResidualBlocks =
relativeBlockNorms(trueResidual, rightHandSide, operation.GetOffsets(), communicator);
mfem::Vector preconditionedDirection(operation.Height());
inversePreconditioner.Mult(arnoldiDirection, preconditionedDirection);
mfem::Vector defect(operation.Height());
operation.Mult(preconditionedDirection, defect);
defect -= arnoldiDirection;
const MaterialBlockMeasurements defectBlocks = blockNorms(defect, operation.GetOffsets(), communicator);
const double defectNorm =
globalNorm(defect, communicator) /
std::max(globalNorm(arnoldiDirection, communicator), std::numeric_limits<double>::min());
std::map<std::string, double> solveMetrics{
{"setup_seconds_maximum_rank", setupSeconds},
{"maximum_iterations", static_cast<double>(maximumIterations)},
{"restart_dimension", static_cast<double>(restartDimension)},
{"solver_converged", krylov.GetConverged() ? 1.0 : 0.0},
{"outer_iterations", static_cast<double>(krylov.GetNumIterations())},
{"reported_initial_residual_norm", std::abs(krylov.GetInitialNorm())},
{"reported_final_residual_norm", std::abs(krylov.GetFinalNorm())},
{"true_relative_residual", trueRelativeResidual},
{"relative_solution_error", relativeSolutionError},
{"density_relative_residual", relativeResidualBlocks.density},
{"surface_relative_residual", relativeResidualBlocks.surface},
{"enthalpy_relative_residual", relativeResidualBlocks.enthalpy},
{"right_preconditioned_defect", defectNorm},
{"density_defect_norm", defectBlocks.density},
{"surface_defect_norm", defectBlocks.surface},
{"enthalpy_defect_norm", defectBlocks.enthalpy},
{"solve_seconds_maximum_rank", solveSeconds},
{"jacobian_applications", static_cast<double>(instrumentedOperation.GetStatistics().applications)},
{"jacobian_application_seconds", instrumentedOperation.GetStatistics().totalSeconds},
{"preconditioner_applications",
static_cast<double>(instrumentedPreconditioner.GetStatistics().applications)},
{"preconditioner_application_seconds", instrumentedPreconditioner.GetStatistics().totalSeconds},
{"preconditioner_maximum_application_seconds", instrumentedPreconditioner.GetStatistics().maximumSeconds}
};
solveMetrics.insert(preparationMetrics.begin(), preparationMetrics.end());
experiment::record_experiment_result(
"material_surface_preconditioning_p9", candidate + "_linear_solve",
commonParameters(candidate, "manufactured_linear_solve", operation.Width()), std::move(solveMetrics)
);
const double initialNorm = std::max(std::abs(krylov.GetInitialNorm()), 1.0e-300);
const auto &history = monitor.GetHistory();
for (std::size_t index = 0; index < history.size(); ++index) {
const auto &sample = history[index];
experiment::record_experiment_result(
"material_surface_preconditioning_p9", candidate + "_history_" + std::to_string(index),
commonParameters(candidate, "fgmres_residual_history", operation.Width()),
{{"history_sample", static_cast<double>(index)},
{"iteration", static_cast<double>(sample.iteration)},
{"reported_residual_norm", sample.reportedNorm},
{"reported_relative_residual", std::abs(sample.reportedNorm) / initialNorm},
{"final_measurement", sample.final ? 1.0 : 0.0}}
);
}
instrumentedOperation.ResetStatistics();
instrumentedPreconditioner.ResetStatistics();
solver::FixedRightPreconditionedOperator product(instrumentedOperation, instrumentedPreconditioner);
announce(communicator, "measuring " + candidate + " with 16-vector Arnoldi");
const solver::ArnoldiSpectralMeasurement spectrum = solver::measureArnoldiSpectrum(
product, arnoldiDirection, communicator,
{.krylovDimension = arnoldiDimension,
.breakdownRelativeTolerance = 1.0e-13,
.ritzConvergenceRelativeTolerance = 1.0e-7,
.reorthogonalize = true}
);
REQUIRE(std::isfinite(trueRelativeResidual));
REQUIRE(std::isfinite(relativeSolutionError));
REQUIRE(std::isfinite(defectNorm));
REQUIRE(std::isfinite(spectrum.projectedConditionProxy));
recordSpectrum(candidate, spectrum, operation.Width(), setupSeconds);
int rank = 0;
MPI_Comm_rank(communicator, &rank);
if (rank == 0) {
std::cout << "[P9 material-surface] " << candidate << ": iterations=" << krylov.GetNumIterations()
<< ", converged=" << (krylov.GetConverged() ? "yes" : "no")
<< ", true residual=" << trueRelativeResidual << ", defect=" << defectNorm
<< ", projected condition=" << spectrum.projectedConditionProxy << '\n';
}
}
template <preconditioning::MaterialSurfaceFactorizationPolicy Policy>
void prepareAndMeasure(
const std::string &candidate,
const Policy policy,
const auto &problem,
const mean_field::preconditioning::MaterialSurfaceJacobianOperator &operation,
const mfem::Vector &exactCorrection,
const mfem::Vector &rightHandSide,
const mfem::Vector &arnoldiDirection,
const MPI_Comm communicator,
const preconditioning::MaterialSurfaceDiagonalOptions diagonalOptions = {}
) {
const Clock::time_point setupStart = Clock::now();
auto block = preconditioning::materialSurfaceBlock(
problem, backend::Diagonal{}, backend::Diagonal{}, policy, diagonalOptions
);
auto prepared = preconditioning::prepare(problem, block);
const double setupTime = maximumRankSeconds(setupStart, communicator);
const auto &density = prepared.GetDensityDiagonalQuality();
const auto &surface = prepared.GetSurfaceDiagonalQuality();
const auto &enthalpy = prepared.GetEnthalpyDiagonalQuality();
const auto &calibration = prepared.GetSurfaceCalibration();
measureCandidate(
candidate, prepared, setupTime, operation, exactCorrection, rightHandSide, arnoldiDirection, communicator,
{{"density_diagonal_minimum", density.minimumAbsoluteEntryBeforeRegularization},
{"density_diagonal_maximum", density.maximumAbsoluteEntryBeforeRegularization},
{"density_diagonal_floor", density.appliedFloor},
{"density_regularized_entries", static_cast<double>(density.regularizedEntries)},
{"surface_diagonal_minimum", surface.minimumAbsoluteEntryBeforeRegularization},
{"surface_diagonal_maximum", surface.maximumAbsoluteEntryBeforeRegularization},
{"surface_diagonal_floor", surface.appliedFloor},
{"surface_regularized_entries", static_cast<double>(surface.regularizedEntries)},
{"surface_calibration_target", static_cast<double>(calibration.target)},
{"surface_calibration_probes", static_cast<double>(calibration.probeCount)},
{"surface_calibration_objective", static_cast<double>(calibration.objective)},
{"surface_calibration_scale", calibration.scale},
{"surface_calibration_inverse_multiplier", calibration.inverseMultiplier},
{"surface_calibration_numerator", calibration.leastSquaresNumerator},
{"surface_calibration_denominator", calibration.leastSquaresDenominator},
{"enthalpy_diagonal_minimum", enthalpy.minimumAbsoluteEntryBeforeRegularization},
{"enthalpy_diagonal_maximum", enthalpy.maximumAbsoluteEntryBeforeRegularization},
{"enthalpy_diagonal_floor", enthalpy.appliedFloor},
{"enthalpy_regularized_entries", static_cast<double>(enthalpy.regularizedEntries)}}
);
}
template <preconditioning::MaterialSurfaceFactorizationPolicy Policy>
void prepareAndMeasureH1(
const std::string &candidate,
const Policy policy,
const int fixedAMGCycles,
const int calibrationProbeCount,
const auto &problem,
const mean_field::preconditioning::MaterialSurfaceJacobianOperator &operation,
const mfem::Vector &exactCorrection,
const mfem::Vector &rightHandSide,
const mfem::Vector &arnoldiDirection,
const MPI_Comm communicator
) {
REQUIRE(fixedAMGCycles > 0);
REQUIRE(calibrationProbeCount >= 3);
const Clock::time_point setupStart = Clock::now();
auto block = preconditioning::materialSurfaceBlock(
problem, backend::Diagonal{}, backend::HypreBoomerAMG{backend::FixedCycles{.cycles = fixedAMGCycles}},
policy,
preconditioning::SurfaceH1MassStiffness{
.calibration = {
.target = preconditioning::SurfaceRieszCalibrationTarget::surface_jacobian,
.probeCount = calibrationProbeCount
}
}
);
auto prepared = preconditioning::prepare(problem, std::move(block));
const double setupTime = maximumRankSeconds(setupStart, communicator);
const auto &density = prepared.GetDensityDiagonalQuality();
const auto &enthalpy = prepared.GetEnthalpyDiagonalQuality();
const auto &fit = prepared.GetSurfaceFit();
measureCandidate(
candidate, prepared, setupTime, operation, exactCorrection, rightHandSide, arnoldiDirection, communicator,
{{"density_diagonal_minimum", density.minimumAbsoluteEntryBeforeRegularization},
{"density_diagonal_maximum", density.maximumAbsoluteEntryBeforeRegularization},
{"density_diagonal_floor", density.appliedFloor},
{"density_regularized_entries", static_cast<double>(density.regularizedEntries)},
{"surface_h1_calibration_target", static_cast<double>(fit.target)},
{"surface_h1_calibration_probes", static_cast<double>(fit.probeCount)},
{"surface_h1_fit_sign", fit.sign},
{"surface_h1_mass_coefficient", fit.massCoefficient},
{"surface_h1_stiffness_coefficient", fit.stiffnessCoefficient},
{"surface_h1_fit_relative_residual", fit.relativeResidual},
{"surface_h1_fit_relative_gram_determinant", fit.relativeGramDeterminant},
{"surface_amg_fixed_cycles", static_cast<double>(fixedAMGCycles)},
{"enthalpy_diagonal_minimum", enthalpy.minimumAbsoluteEntryBeforeRegularization},
{"enthalpy_diagonal_maximum", enthalpy.maximumAbsoluteEntryBeforeRegularization},
{"enthalpy_diagonal_floor", enthalpy.appliedFloor},
{"enthalpy_regularized_entries", static_cast<double>(enthalpy.regularizedEntries)}}
);
const auto &surfaceBackendStatistics = prepared.GetSurfaceBackend().GetStatistics();
const auto &factorizationStatistics = prepared.GetFactorization().GetStatistics();
const auto &preparationStatistics = prepared.GetStatistics();
auto parameters = commonParameters(candidate, "surface_h1_backend_statistics", operation.Width());
parameters["experiment_schema"] = "p9_material_surface_h1_v1";
parameters["surface_surrogate"] = "h1_mass_plus_tangential_stiffness";
parameters["surface_calibration_target"] = "surface_jacobian";
parameters["surface_calibration_probes"] = std::to_string(calibrationProbeCount);
parameters["surface_amg_fixed_cycles"] = std::to_string(fixedAMGCycles);
experiment::record_experiment_result(
"material_surface_preconditioning_p9", candidate + "_surface_h1_backend_statistics", std::move(parameters),
{{"setup_seconds_maximum_rank", setupTime},
{"surface_h1_fit_sign", fit.sign},
{"surface_h1_mass_coefficient", fit.massCoefficient},
{"surface_h1_stiffness_coefficient", fit.stiffnessCoefficient},
{"surface_h1_fit_relative_residual", fit.relativeResidual},
{"surface_h1_fit_relative_gram_determinant", fit.relativeGramDeterminant},
{"surface_backend_setups", static_cast<double>(surfaceBackendStatistics.setups)},
{"surface_backend_applications", static_cast<double>(surfaceBackendStatistics.applications)},
{"surface_backend_inner_iterations", static_cast<double>(surfaceBackendStatistics.innerIterations)},
{"surface_backend_last_inner_iterations",
static_cast<double>(surfaceBackendStatistics.lastInnerIterations)},
{"factorization_applications", static_cast<double>(factorizationStatistics.applications)},
{"surface_inverse_applications", static_cast<double>(factorizationStatistics.surfaceInverseApplications)},
{"block_setups", static_cast<double>(preparationStatistics.setups)},
{"surface_jacobian_probes", static_cast<double>(preparationStatistics.surfaceJacobianProbes)},
{"surface_h1_assemblies", static_cast<double>(preparationStatistics.surfaceH1Assemblies)}}
);
}
enum class SurfaceProbeMode { constant, ordered_low, alternating_high, deterministic_mixed };
[[nodiscard]] const char *surfaceProbeModeName(const SurfaceProbeMode mode) noexcept {
switch (mode) {
case SurfaceProbeMode::constant:
return "constant";
case SurfaceProbeMode::ordered_low:
return "ordered_low";
case SurfaceProbeMode::alternating_high:
return "alternating_high";
case SurfaceProbeMode::deterministic_mixed:
return "deterministic_mixed";
}
return "unknown";
}
[[nodiscard]] mfem::Vector normalizedSurfaceProbe(
const int localSize,
const SurfaceProbeMode mode,
const MPI_Comm communicator
) {
int globalSize = 0;
int offset = 0;
MPI_Allreduce(&localSize, &globalSize, 1, MPI_INT, MPI_SUM, communicator);
MPI_Exscan(&localSize, &offset, 1, MPI_INT, MPI_SUM, communicator);
int rank = 0;
MPI_Comm_rank(communicator, &rank);
if (rank == 0) {
offset = 0;
}
REQUIRE(globalSize > 0);
mfem::Vector probe(localSize);
for (int index = 0; index < localSize; ++index) {
const int globalIndex = offset + index;
const double position = (static_cast<double>(globalIndex) + 0.5) / static_cast<double>(globalSize);
switch (mode) {
case SurfaceProbeMode::constant:
probe(index) = 1.0;
break;
case SurfaceProbeMode::ordered_low:
probe(index) = std::cos(std::numbers::pi_v<double> * position);
break;
case SurfaceProbeMode::alternating_high:
probe(index) = globalIndex % 2 == 0 ? 1.0 : -1.0;
break;
case SurfaceProbeMode::deterministic_mixed:
probe(index) = 0.41 * std::cos(std::numbers::pi_v<double> * position) +
std::sin(5.0 * std::numbers::pi_v<double> * position) +
0.23 * (globalIndex % 2 == 0 ? 1.0 : -1.0);
break;
}
}
const double norm = globalNorm(probe, communicator);
REQUIRE(norm > 0.0);
probe /= norm;
return probe;
}
void applyParameterOverrides(
std::map<
std::string,
std::string> &parameters,
const std::map<
std::string,
std::string> &overrides
) {
for (const auto &[key, value] : overrides) {
parameters.insert_or_assign(key, value);
}
}
template <typename SurfaceInverse>
void recordSurfaceInverseRecovery(
const std::string &candidate,
SurfaceInverse &surfaceInverse,
const mean_field::preconditioning::MaterialSurfaceJacobianOperator &operation,
const MPI_Comm communicator,
const std::map<
std::string,
std::string> &parameterOverrides
) {
constexpr std::array modes{
SurfaceProbeMode::constant, SurfaceProbeMode::ordered_low, SurfaceProbeMode::alternating_high,
SurfaceProbeMode::deterministic_mixed
};
const int surfaceSize = operation.GetOffsets()[2] - operation.GetOffsets()[1];
REQUIRE(surfaceInverse.Width() == surfaceSize);
REQUIRE(surfaceInverse.Height() == surfaceSize);
for (const SurfaceProbeMode mode : modes) {
const mfem::Vector probe = normalizedSurfaceProbe(surfaceSize, mode, communicator);
mfem::Vector surfaceAction(surfaceSize);
mfem::Vector recovered(surfaceSize);
operation.ApplySurfaceToSurface(probe, surfaceAction);
const Clock::time_point inverseStart = Clock::now();
surfaceInverse.Mult(surfaceAction, recovered);
const double inverseSeconds = maximumRankSeconds(inverseStart, communicator);
mfem::Vector recoveryError(recovered);
recoveryError -= probe;
const double probeNorm = globalNorm(probe, communicator);
const double actionNorm = globalNorm(surfaceAction, communicator);
const double recoveredNorm = globalNorm(recovered, communicator);
const double relativeError = globalNorm(recoveryError, communicator) / probeNorm;
constexpr double nonzeroFloor = 1.0e-300;
auto parameters = commonParameters(candidate, "surface_inverse_recovery", operation.Width());
applyParameterOverrides(parameters, parameterOverrides);
parameters["surface_probe_mode"] = surfaceProbeModeName(mode);
experiment::record_experiment_result(
"material_surface_preconditioning_p9", candidate + "_" + surfaceProbeModeName(mode),
std::move(parameters),
{{"surface_probe_norm", probeNorm},
{"surface_action_norm", actionNorm},
{"surface_recovered_norm", recoveredNorm},
{"surface_recovery_relative_error", relativeError},
{"surface_operator_gain", actionNorm / probeNorm},
{"surface_inverse_gain", recoveredNorm / std::max(actionNorm, nonzeroFloor)},
{"surface_recovered_gain", recoveredNorm / probeNorm},
{"surface_inverse_seconds_maximum_rank", inverseSeconds}}
);
}
}
template <typename PreparedPreconditioner>
void recordBalancedPreconditionedDefect(
const std::string &candidate,
PreparedPreconditioner &prepared,
const mean_field::preconditioning::MaterialSurfaceJacobianOperator &operation,
const MPI_Comm communicator,
const std::map<
std::string,
std::string> &parameterOverrides
) {
const mfem::Vector direction = blockBalancedDirection(operation.GetOffsets(), 1.37, communicator);
mfem::Vector correction(operation.Width());
mfem::Vector defect(operation.Height());
const Clock::time_point applicationStart = Clock::now();
prepared.Mult(direction, correction);
const double applicationSeconds = maximumRankSeconds(applicationStart, communicator);
operation.Mult(correction, defect);
defect -= direction;
const MaterialBlockMeasurements blockDefects = blockNorms(defect, operation.GetOffsets(), communicator);
const double relativeDefect = globalNorm(defect, communicator) /
std::max(globalNorm(direction, communicator), std::numeric_limits<double>::min());
auto parameters = commonParameters(candidate, "balanced_right_preconditioned_defect", operation.Width());
applyParameterOverrides(parameters, parameterOverrides);
experiment::record_experiment_result(
"material_surface_preconditioning_p9", candidate + "_balanced_defect", std::move(parameters),
{{"right_preconditioned_defect", relativeDefect},
{"density_defect_norm", blockDefects.density},
{"surface_defect_norm", blockDefects.surface},
{"enthalpy_defect_norm", blockDefects.enthalpy},
{"preconditioner_application_seconds_maximum_rank", applicationSeconds}}
);
}
void measureH1CalibrationFloor(
const std::string &candidate,
const std::string &relativeMassCoefficientFloorLabel,
const double relativeMassCoefficientFloor,
const auto &problem,
const mean_field::preconditioning::MaterialSurfaceJacobianOperator &operation,
const MPI_Comm communicator
) {
const Clock::time_point setupStart = Clock::now();
auto block = preconditioning::materialSurfaceBlock(
problem, backend::Diagonal{}, backend::HypreBoomerAMG{backend::FixedCycles{.cycles = 1}},
preconditioning::ApproximateMaterialSurfaceLDU{},
preconditioning::SurfaceH1MassStiffness{
.calibration =
{.target = preconditioning::SurfaceRieszCalibrationTarget::surface_jacobian, .probeCount = 4},
.relativeMassCoefficientFloor = relativeMassCoefficientFloor
}
);
auto prepared = preconditioning::prepare(problem, std::move(block));
const double setupTime = maximumRankSeconds(setupStart, communicator);
const auto &fit = prepared.GetSurfaceFit();
const std::map<std::string, std::string> parameters{
{"experiment_schema", "p9_material_surface_h1_tuning_v1"},
{"surface_surrogate", "h1_mass_plus_tangential_stiffness"},
{"surface_calibration_target", "surface_jacobian"},
{"surface_calibration_probes", "4"},
{"surface_amg_fixed_cycles", "1"},
{"relative_mass_coefficient_floor", relativeMassCoefficientFloorLabel}
};
recordSurfaceInverseRecovery(candidate, prepared.GetSurfaceInverse(), operation, communicator, parameters);
recordBalancedPreconditionedDefect(candidate, prepared, operation, communicator, parameters);
const auto &backendStatistics = prepared.GetSurfaceBackend().GetStatistics();
const auto &factorizationStatistics = prepared.GetFactorization().GetStatistics();
const auto &preparationStatistics = prepared.GetStatistics();
auto summaryParameters = commonParameters(candidate, "surface_h1_floor_summary", operation.Width());
applyParameterOverrides(summaryParameters, parameters);
experiment::record_experiment_result(
"material_surface_preconditioning_p9", candidate + "_summary", std::move(summaryParameters),
{{"setup_seconds_maximum_rank", setupTime},
{"relative_mass_coefficient_floor", relativeMassCoefficientFloor},
{"surface_h1_fit_sign", fit.sign},
{"surface_h1_mass_coefficient", fit.massCoefficient},
{"surface_h1_stiffness_coefficient", fit.stiffnessCoefficient},
{"surface_h1_fit_relative_residual", fit.relativeResidual},
{"surface_h1_fit_relative_gram_determinant", fit.relativeGramDeterminant},
{"surface_backend_setups", static_cast<double>(backendStatistics.setups)},
{"surface_backend_applications", static_cast<double>(backendStatistics.applications)},
{"surface_backend_inner_iterations", static_cast<double>(backendStatistics.innerIterations)},
{"surface_backend_last_inner_iterations", static_cast<double>(backendStatistics.lastInnerIterations)},
{"factorization_applications", static_cast<double>(factorizationStatistics.applications)},
{"surface_inverse_applications", static_cast<double>(factorizationStatistics.surfaceInverseApplications)},
{"surface_jacobian_probes", static_cast<double>(preparationStatistics.surfaceJacobianProbes)},
{"surface_h1_assemblies", static_cast<double>(preparationStatistics.surfaceH1Assemblies)}}
);
}
void measureScalarSurfaceControl(
const std::string &candidate,
const preconditioning::SurfaceRieszCalibrationObjective objective,
const int calibrationProbeCount,
const auto &problem,
const mean_field::preconditioning::MaterialSurfaceJacobianOperator &operation,
const MPI_Comm communicator
) {
REQUIRE(calibrationProbeCount > 0);
const preconditioning::MaterialSurfaceDiagonalOptions calibration{
.surfaceCalibration = {
.target = preconditioning::SurfaceRieszCalibrationTarget::surface_jacobian,
.probeCount = calibrationProbeCount,
.objective = objective
}
};
const Clock::time_point setupStart = Clock::now();
auto block = preconditioning::materialSurfaceBlock(
problem, backend::Diagonal{}, backend::Diagonal{}, preconditioning::ApproximateMaterialSurfaceLDU{},
calibration
);
auto prepared = preconditioning::prepare(problem, std::move(block));
auto directSurfaceInverse = backend::prepare(backend::Diagonal{}, prepared.GetSurfaceDiagonal());
const double setupTime = maximumRankSeconds(setupStart, communicator);
const auto &calibrationData = prepared.GetSurfaceCalibration();
const std::map<std::string, std::string> parameters{
{"experiment_schema", "p9_material_surface_h1_tuning_v1"},
{"surface_surrogate", "scalar_mass_diagonal"},
{"surface_calibration_target", "surface_jacobian"},
{"surface_calibration_probes", std::to_string(calibrationProbeCount)},
{"surface_calibration_objective",
objective == preconditioning::SurfaceRieszCalibrationObjective::operator_action
? "operator_action"
: "right_preconditioned_action"}
};
recordSurfaceInverseRecovery(candidate, directSurfaceInverse, operation, communicator, parameters);
recordBalancedPreconditionedDefect(candidate, prepared, operation, communicator, parameters);
const auto &directStatistics = directSurfaceInverse.GetStatistics();
const auto &factorizationStatistics = prepared.GetFactorization().GetStatistics();
auto summaryParameters = commonParameters(candidate, "scalar_surface_control_summary", operation.Width());
applyParameterOverrides(summaryParameters, parameters);
experiment::record_experiment_result(
"material_surface_preconditioning_p9", candidate + "_summary", std::move(summaryParameters),
{{"setup_seconds_maximum_rank", setupTime},
{"surface_calibration_scale", calibrationData.scale},
{"surface_calibration_inverse_multiplier", calibrationData.inverseMultiplier},
{"surface_calibration_numerator", calibrationData.leastSquaresNumerator},
{"surface_calibration_denominator", calibrationData.leastSquaresDenominator},
{"surface_backend_setups", static_cast<double>(directStatistics.setups)},
{"surface_backend_applications", static_cast<double>(directStatistics.applications)},
{"factorization_applications", static_cast<double>(factorizationStatistics.applications)},
{"surface_inverse_applications", static_cast<double>(factorizationStatistics.surfaceInverseApplications)}}
);
}
} // namespace
TEST_CASE(
"Material Surface P9 Numerical Factorization Comparison",
"[preconditioning][material_surface][diagnostics][experiment][spectrum][p9][p9_baseline]"
) {
using namespace mean_field;
const utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
const MPI_Comm communicator = finiteElements.mesh->GetComm();
constexpr double radius = utils::RADIUS;
constexpr double mass = utils::MASS;
const double polytropicConstant = 2.0 * utils::G * radius * radius / std::numbers::pi_v<double>;
const double centralDensity = std::numbers::pi_v<double> * mass / (4.0 * radius * radius * radius);
auto model = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
);
auto problem = equilibrium::discretize(model, finiteElements);
auto projected = seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = 1024}));
problem.Prepare(projected.values, makeDependencies(), zeroRotation());
const auto &physical = problem.GetPreparedOperator().GetPhysicalOperator();
preconditioning::MaterialSurfaceJacobianOperator materialSurfaceOperator(physical);
const auto exactCorrection = blockBalancedDirection(materialSurfaceOperator.GetOffsets(), 0.23, communicator);
mfem::Vector rightHandSide(materialSurfaceOperator.Height());
materialSurfaceOperator.Mult(exactCorrection, rightHandSide);
const auto arnoldiDirection = blockBalancedDirection(materialSurfaceOperator.GetOffsets(), 0.79, communicator);
solver::IdentityPreconditioner identity(materialSurfaceOperator.Width());
measureCandidate(
"identity", identity, 0.0, materialSurfaceOperator, exactCorrection, rightHandSide, arnoldiDirection,
communicator
);
prepareAndMeasure(
"block_diagonal", preconditioning::MaterialSurfaceBlockDiagonal{}, problem, materialSurfaceOperator,
exactCorrection, rightHandSide, arnoldiDirection, communicator
);
prepareAndMeasure(
"material_independent_surface", preconditioning::CoupledMaterialIndependentSurface{}, problem,
materialSurfaceOperator, exactCorrection, rightHandSide, arnoldiDirection, communicator
);
prepareAndMeasure(
"material_then_surface", preconditioning::MaterialThenSurfaceTriangular{}, problem, materialSurfaceOperator,
exactCorrection, rightHandSide, arnoldiDirection, communicator
);
prepareAndMeasure(
"surface_then_material", preconditioning::SurfaceThenMaterialTriangular{}, problem, materialSurfaceOperator,
exactCorrection, rightHandSide, arnoldiDirection, communicator
);
prepareAndMeasure(
"approximate_ldu", preconditioning::ApproximateMaterialSurfaceLDU{}, problem, materialSurfaceOperator,
exactCorrection, rightHandSide, arnoldiDirection, communicator
);
}
TEST_CASE(
"Material Surface P9 Calibrated LDU Comparison",
"[preconditioning][material_surface][diagnostics][experiment][spectrum][p9][p9_refinement]"
) {
using namespace mean_field;
const utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
const MPI_Comm communicator = finiteElements.mesh->GetComm();
constexpr double radius = utils::RADIUS;
constexpr double mass = utils::MASS;
const double polytropicConstant = 2.0 * utils::G * radius * radius / std::numbers::pi_v<double>;
const double centralDensity = std::numbers::pi_v<double> * mass / (4.0 * radius * radius * radius);
auto model = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
);
auto problem = equilibrium::discretize(model, finiteElements);
auto projected = seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = 1024}));
problem.Prepare(projected.values, makeDependencies(), zeroRotation());
const auto &physical = problem.GetPreparedOperator().GetPhysicalOperator();
preconditioning::MaterialSurfaceJacobianOperator materialSurfaceOperator(physical);
const auto exactCorrection = blockBalancedDirection(materialSurfaceOperator.GetOffsets(), 0.23, communicator);
mfem::Vector rightHandSide(materialSurfaceOperator.Height());
materialSurfaceOperator.Mult(exactCorrection, rightHandSide);
const auto arnoldiDirection = blockBalancedDirection(materialSurfaceOperator.GetOffsets(), 0.79, communicator);
constexpr preconditioning::MaterialSurfaceDiagonalOptions surfaceJacobianCalibration{
.surfaceCalibration = {
.target = preconditioning::SurfaceRieszCalibrationTarget::surface_jacobian, .probeCount = 4
}
};
constexpr preconditioning::MaterialSurfaceDiagonalOptions surfaceSchurCalibration{
.surfaceCalibration = {
.target = preconditioning::SurfaceRieszCalibrationTarget::approximate_material_schur, .probeCount = 4
}
};
prepareAndMeasure(
"surface_then_material_calibrated_aqq", preconditioning::SurfaceThenMaterialTriangular{}, problem,
materialSurfaceOperator, exactCorrection, rightHandSide, arnoldiDirection, communicator,
surfaceJacobianCalibration
);
prepareAndMeasure(
"approximate_ldu", preconditioning::ApproximateMaterialSurfaceLDU{}, problem, materialSurfaceOperator,
exactCorrection, rightHandSide, arnoldiDirection, communicator
);
prepareAndMeasure(
"approximate_ldu_calibrated_aqq", preconditioning::ApproximateMaterialSurfaceLDU{}, problem,
materialSurfaceOperator, exactCorrection, rightHandSide, arnoldiDirection, communicator,
surfaceJacobianCalibration
);
prepareAndMeasure(
"approximate_ldu_calibrated_schur", preconditioning::ApproximateMaterialSurfaceLDU{}, problem,
materialSurfaceOperator, exactCorrection, rightHandSide, arnoldiDirection, communicator, surfaceSchurCalibration
);
}
TEST_CASE(
"Material Surface P9 Frequency-Aware Surface Refinement",
"[preconditioning][material_surface][diagnostics][experiment][spectrum][p9][p9_h1_refinement]"
) {
using namespace mean_field;
const utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
const MPI_Comm communicator = finiteElements.mesh->GetComm();
constexpr double radius = utils::RADIUS;
constexpr double mass = utils::MASS;
const double polytropicConstant = 2.0 * utils::G * radius * radius / std::numbers::pi_v<double>;
const double centralDensity = std::numbers::pi_v<double> * mass / (4.0 * radius * radius * radius);
auto model = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
);
auto problem = equilibrium::discretize(model, finiteElements);
auto projected = seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = 1024}));
problem.Prepare(projected.values, makeDependencies(), zeroRotation());
const auto &physical = problem.GetPreparedOperator().GetPhysicalOperator();
preconditioning::MaterialSurfaceJacobianOperator materialSurfaceOperator(physical);
const auto exactCorrection = blockBalancedDirection(materialSurfaceOperator.GetOffsets(), 0.23, communicator);
mfem::Vector rightHandSide(materialSurfaceOperator.Height());
materialSurfaceOperator.Mult(exactCorrection, rightHandSide);
const auto arnoldiDirection = blockBalancedDirection(materialSurfaceOperator.GetOffsets(), 0.79, communicator);
constexpr int calibrationProbeCount = 4;
prepareAndMeasureH1(
"h1_aqq_surface_then_material_amg1", preconditioning::SurfaceThenMaterialTriangular{}, 1, calibrationProbeCount,
problem, materialSurfaceOperator, exactCorrection, rightHandSide, arnoldiDirection, communicator
);
prepareAndMeasureH1(
"h1_aqq_approximate_ldu_amg1", preconditioning::ApproximateMaterialSurfaceLDU{}, 1, calibrationProbeCount,
problem, materialSurfaceOperator, exactCorrection, rightHandSide, arnoldiDirection, communicator
);
prepareAndMeasureH1(
"h1_aqq_approximate_ldu_amg2", preconditioning::ApproximateMaterialSurfaceLDU{}, 2, calibrationProbeCount,
problem, materialSurfaceOperator, exactCorrection, rightHandSide, arnoldiDirection, communicator
);
}
TEST_CASE(
"Material Surface P9 H1 Calibration Floor Tuning",
"[preconditioning][material_surface][diagnostics][experiment][p9][p9_h1_tuning]"
) {
using namespace mean_field;
const utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
const MPI_Comm communicator = finiteElements.mesh->GetComm();
constexpr double radius = utils::RADIUS;
constexpr double mass = utils::MASS;
const double polytropicConstant = 2.0 * utils::G * radius * radius / std::numbers::pi_v<double>;
const double centralDensity = std::numbers::pi_v<double> * mass / (4.0 * radius * radius * radius);
auto model = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
);
auto problem = equilibrium::discretize(model, finiteElements);
auto projected = seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = 1024}));
problem.Prepare(projected.values, makeDependencies(), zeroRotation());
const auto &physical = problem.GetPreparedOperator().GetPhysicalOperator();
preconditioning::MaterialSurfaceJacobianOperator materialSurfaceOperator(physical);
constexpr std::array floorCases{
std::pair{"1e-10", 1.0e-10}, std::pair{"1e-4", 1.0e-4}, std::pair{"1e-2", 1.0e-2}, std::pair{"1e-1", 1.0e-1},
std::pair{"1", 1.0}
};
for (const auto &[label, floor] : floorCases) {
measureH1CalibrationFloor(
std::string("h1_aqq_floor_") + label, label, floor, problem, materialSurfaceOperator, communicator
);
}
measureScalarSurfaceControl(
"scalar_aqq_operator_calibrated_diagonal_control",
preconditioning::SurfaceRieszCalibrationObjective::operator_action, 4, problem, materialSurfaceOperator,
communicator
);
constexpr std::array inverseProbeCounts{1, 2, 4, 8, 16};
for (const int probeCount : inverseProbeCounts) {
measureScalarSurfaceControl(
"scalar_aqq_right_calibrated_diagonal_" + std::to_string(probeCount) + "_probes",
preconditioning::SurfaceRieszCalibrationObjective::right_preconditioned_action, probeCount, problem,
materialSurfaceOperator, communicator
);
}
}