1219 lines
66 KiB
C++
1219 lines
66 KiB
C++
#include <algorithm>
|
|
#include <array>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <exception>
|
|
#include <iostream>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <numbers>
|
|
#include <ranges>
|
|
#include <span>
|
|
#include <string>
|
|
#include <string_view>
|
|
#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 CandidateDescription final {
|
|
std::string name;
|
|
std::string materialFactorization;
|
|
std::string surfaceCalibration;
|
|
std::string stellarStructureFactorization;
|
|
std::string gravityFactorization;
|
|
std::string specificationBorder;
|
|
};
|
|
|
|
struct SetupTimings final {
|
|
double finiteElementSeconds{0.0};
|
|
double laneEmdenCalibrationSeconds{0.0};
|
|
double problemConstructionSeconds{0.0};
|
|
double seedProjectionSeconds{0.0};
|
|
double operatorPreparationSeconds{0.0};
|
|
double manufacturedRightHandSideSeconds{0.0};
|
|
};
|
|
|
|
struct NewtonCandidateResult final {
|
|
CandidateDescription candidate;
|
|
mfem::Vector correction;
|
|
mfem::Vector linearAction;
|
|
};
|
|
|
|
[[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 localSquared = vector * vector;
|
|
double globalSquared = 0.0;
|
|
MPI_Allreduce(&localSquared, &globalSquared, 1, MPI_DOUBLE, MPI_SUM, communicator);
|
|
return std::sqrt(std::max(globalSquared, 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 << "[P10 full stellar] " << message << std::endl;
|
|
}
|
|
}
|
|
|
|
class ArnoldiProgressOperator final : public mfem::Operator {
|
|
public:
|
|
ArnoldiProgressOperator(
|
|
const mfem::Operator &operation,
|
|
const MPI_Comm communicator,
|
|
const int expectedApplications
|
|
)
|
|
: mfem::Operator(
|
|
operation.Height(),
|
|
operation.Width()
|
|
),
|
|
m_operation(&operation),
|
|
m_communicator(communicator),
|
|
m_expectedApplications(expectedApplications) {
|
|
}
|
|
|
|
void Mult(
|
|
const mfem::Vector &input,
|
|
mfem::Vector &output
|
|
) const override {
|
|
m_operation->Mult(input, output);
|
|
++m_completedApplications;
|
|
if (m_completedApplications == 1 || m_completedApplications == m_expectedApplications ||
|
|
m_completedApplications % 4 == 0) {
|
|
announce(
|
|
m_communicator, "Arnoldi progress " + std::to_string(m_completedApplications) + "/" +
|
|
std::to_string(m_expectedApplications)
|
|
);
|
|
}
|
|
}
|
|
|
|
private:
|
|
const mfem::Operator *m_operation;
|
|
MPI_Comm m_communicator;
|
|
int m_expectedApplications;
|
|
mutable int m_completedApplications{0};
|
|
};
|
|
|
|
[[nodiscard]] mean_field::operators::StellarEquilibriumDependencies makeDependencies() {
|
|
return {
|
|
.discretization = {.identity = 111103, .revision = 1},
|
|
.density = {.identity = 111109, .revision = 1},
|
|
.surfaceDeformation = {.identity = 111119, .revision = 1},
|
|
.gravityGradient = {.identity = 111121, .revision = 1},
|
|
.gravityPotential = {.identity = 111127, .revision = 1},
|
|
.enthalpy = {.identity = 111143, .revision = 1},
|
|
.bernoulliConstant = {.identity = 111149, .revision = 1},
|
|
.rotation = {.identity = 111151, .revision = 1},
|
|
.targetMass = {.identity = 111157, .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 int size,
|
|
const std::span<const mean_field::operators::RootBlockDescriptor> blocks,
|
|
const double phase,
|
|
const MPI_Comm communicator
|
|
) {
|
|
mfem::Vector direction(size);
|
|
direction = 0.0;
|
|
for (const auto &block : blocks) {
|
|
REQUIRE(block.offset >= 0);
|
|
REQUIRE(block.size > 0);
|
|
REQUIRE(block.offset + block.size <= size);
|
|
mfem::Vector values(direction.GetData() + block.offset, block.size);
|
|
for (int index = 0; index < values.Size(); ++index) {
|
|
const double ordinal = static_cast<double>(index + 1);
|
|
values(index) =
|
|
std::sin(0.6180339887498948 * ordinal + phase + static_cast<double>(block.canonicalIndex + 1)) +
|
|
0.31 * std::cos(0.1732050807568877 * ordinal - phase);
|
|
}
|
|
const double norm = globalNorm(values, communicator);
|
|
REQUIRE(norm > 0.0);
|
|
values /= norm;
|
|
}
|
|
return direction;
|
|
}
|
|
|
|
[[nodiscard]] std::map<
|
|
std::string,
|
|
std::string>
|
|
commonParameters(
|
|
const CandidateDescription &candidate,
|
|
const std::string &measurement,
|
|
const int dimension
|
|
) {
|
|
return {
|
|
{"build_configuration", buildConfiguration()},
|
|
{"candidate", candidate.name},
|
|
{"equation_of_state", "Polytrope(n=3)"},
|
|
{"experiment_schema", "p9_p10_full_stellar_v1"},
|
|
{"gravity_factorization", candidate.gravityFactorization},
|
|
{"linearization_state", "projected_lane_emden"},
|
|
{"manufactured_rhs", "J_times_value_block_balanced_correction"},
|
|
{"material_factorization", candidate.materialFactorization},
|
|
{"measurement", measurement},
|
|
{"mesh_file", test_utils::setup_args().mesh_file},
|
|
{"operator", "canonical_bordered_stellar_jacobian"},
|
|
{"preconditioned_product", "J M^-1"},
|
|
{"residual_arnoldi_seed", "residual_block_balanced"},
|
|
{"root_dimension", std::to_string(dimension)},
|
|
{"rotation", "zero"},
|
|
{"specification_border", candidate.specificationBorder},
|
|
{"stellar_structure_factorization", candidate.stellarStructureFactorization},
|
|
{"surface_calibration", candidate.surfaceCalibration}
|
|
};
|
|
}
|
|
|
|
[[nodiscard]] std::map<
|
|
std::string,
|
|
std::string>
|
|
newtonParameters(
|
|
const CandidateDescription &candidate,
|
|
const std::string &measurement,
|
|
const int dimension
|
|
) {
|
|
auto parameters = commonParameters(candidate, measurement, dimension);
|
|
parameters["experiment_schema"] = "p10_physical_newton_rhs_v1";
|
|
parameters["manufactured_rhs"] = "none";
|
|
parameters["right_hand_side"] = "negative_nonlinear_residual";
|
|
parameters["preconditioned_product"] = "not_measured";
|
|
parameters["residual_arnoldi_seed"] = "not_applicable";
|
|
return parameters;
|
|
}
|
|
|
|
void incrementStateRevisions(mean_field::operators::StellarEquilibriumDependencies &dependencies) {
|
|
++dependencies.density.revision;
|
|
++dependencies.surfaceDeformation.revision;
|
|
++dependencies.gravityGradient.revision;
|
|
++dependencies.gravityPotential.revision;
|
|
++dependencies.enthalpy.revision;
|
|
++dependencies.bernoulliConstant.revision;
|
|
++dependencies.rotation.revision;
|
|
++dependencies.targetMass.revision;
|
|
}
|
|
|
|
[[nodiscard]] const mean_field::operators::RootBlockDescriptor &findBlock(
|
|
const std::span<const mean_field::operators::RootBlockDescriptor> blocks,
|
|
const std::string_view stableId
|
|
) {
|
|
const auto iterator =
|
|
std::ranges::find(blocks, stableId, &mean_field::operators::RootBlockDescriptor::stableId);
|
|
REQUIRE(iterator != blocks.end());
|
|
return *iterator;
|
|
}
|
|
|
|
[[nodiscard]] int firstThresholdIteration(
|
|
const std::vector<solver::IterationResidualMeasurement> &history,
|
|
const double initialNorm,
|
|
const double threshold
|
|
) {
|
|
if (!std::isfinite(initialNorm) || initialNorm <= 0.0) {
|
|
return -1;
|
|
}
|
|
for (const auto &sample : history) {
|
|
if (std::abs(sample.reportedNorm) / initialNorm <= threshold) {
|
|
return sample.iteration;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
void recordSpectrum(
|
|
const CandidateDescription &candidate,
|
|
const solver::ArnoldiSpectralMeasurement &spectrum,
|
|
const int dimension,
|
|
const double setupSeconds,
|
|
const solver::OperatorApplicationStatistics &jacobianStatistics,
|
|
const solver::OperatorApplicationStatistics &preconditionerStatistics
|
|
) {
|
|
experiment::record_experiment_result(
|
|
"stellar_preconditioning_p10", candidate.name + "_arnoldi_summary",
|
|
commonParameters(candidate, "arnoldi_summary", dimension),
|
|
{{"preconditioner_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},
|
|
{"operator_maximum_application_seconds_maximum_rank",
|
|
spectrum.operatorMaximumApplicationSecondsMaximumRank},
|
|
{"nonapplication_seconds_maximum_rank", spectrum.nonApplicationSecondsMaximumRank},
|
|
{"measured_jacobian_applications", static_cast<double>(jacobianStatistics.applications)},
|
|
{"measured_jacobian_application_seconds", jacobianStatistics.totalSeconds},
|
|
{"measured_jacobian_maximum_application_seconds", jacobianStatistics.maximumSeconds},
|
|
{"measured_preconditioner_applications", static_cast<double>(preconditionerStatistics.applications)},
|
|
{"measured_preconditioner_application_seconds", preconditionerStatistics.totalSeconds},
|
|
{"measured_preconditioner_maximum_application_seconds", preconditionerStatistics.maximumSeconds},
|
|
{"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(
|
|
"stellar_preconditioning_p10", candidate.name + "_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,
|
|
typename Problem>
|
|
void measureCandidate(
|
|
const CandidateDescription &candidate,
|
|
Preconditioner &inversePreconditioner,
|
|
const double preconditionerSetupSeconds,
|
|
const Problem &problem,
|
|
const mfem::Vector &exactCorrection,
|
|
const mfem::Vector &rightHandSide,
|
|
const mfem::Vector &arnoldiDirection,
|
|
const SetupTimings &setupTimings,
|
|
const MPI_Comm communicator
|
|
) {
|
|
constexpr int maximumIterations = 36;
|
|
constexpr int restartDimension = 18;
|
|
constexpr int arnoldiDimension = 12;
|
|
|
|
const mfem::Operator &jacobian = problem.GetLinearizationOperator();
|
|
solver::InstrumentedOperator instrumentedJacobian(jacobian);
|
|
solver::InstrumentedPreconditioner instrumentedPreconditioner(inversePreconditioner);
|
|
solver::ResidualHistoryMonitor monitor;
|
|
mfem::FGMRESSolver krylov(communicator);
|
|
krylov.SetPreconditioner(instrumentedPreconditioner);
|
|
krylov.SetOperator(instrumentedJacobian);
|
|
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(problem.StateSize());
|
|
solution = 0.0;
|
|
announce(communicator, "solving the manufactured system with " + candidate.name);
|
|
const Clock::time_point solveStart = Clock::now();
|
|
krylov.Mult(rightHandSide, solution);
|
|
const double localSolveSeconds = std::chrono::duration<double>(Clock::now() - solveStart).count();
|
|
|
|
const solver::LinearSolveMeasurement solve = solver::measureLinearSolve(
|
|
krylov, jacobian, rightHandSide, solution, problem.GetManifest().residualBlocks(),
|
|
instrumentedJacobian.GetStatistics(), instrumentedPreconditioner.GetStatistics(),
|
|
instrumentedPreconditioner.GetLifecycleStatistics(), monitor, localSolveSeconds, communicator
|
|
);
|
|
mfem::Vector correctionError(solution);
|
|
correctionError -= exactCorrection;
|
|
const double relativeCorrectionError =
|
|
globalNorm(correctionError, communicator) /
|
|
std::max(globalNorm(exactCorrection, communicator), std::numeric_limits<double>::min());
|
|
|
|
const double reportedInitial = std::max(std::abs(krylov.GetInitialNorm()), 1.0e-300);
|
|
const int iteration1e2 = firstThresholdIteration(solve.reportedResidualHistory, reportedInitial, 1.0e-2);
|
|
const int iteration1e4 = firstThresholdIteration(solve.reportedResidualHistory, reportedInitial, 1.0e-4);
|
|
const int iteration1e6 = firstThresholdIteration(solve.reportedResidualHistory, reportedInitial, 1.0e-6);
|
|
const int iteration1e8 = firstThresholdIteration(solve.reportedResidualHistory, reportedInitial, 1.0e-8);
|
|
|
|
REQUIRE(std::isfinite(solve.directResidual.relativeResidual));
|
|
REQUIRE(std::isfinite(relativeCorrectionError));
|
|
std::map<std::string, double> metrics{
|
|
{"maximum_iterations", static_cast<double>(maximumIterations)},
|
|
{"restart_dimension", static_cast<double>(restartDimension)},
|
|
{"solver_converged", solve.solverConverged ? 1.0 : 0.0},
|
|
{"outer_iterations", static_cast<double>(solve.outerIterations)},
|
|
{"reported_initial_residual_norm", solve.solverReportedInitialNorm},
|
|
{"reported_final_residual_norm", solve.solverReportedFinalNorm},
|
|
{"reported_residual_reduction", solve.solverReportedResidualReduction},
|
|
{"reported_iteration_to_1e-2", static_cast<double>(iteration1e2)},
|
|
{"reported_iteration_to_1e-4", static_cast<double>(iteration1e4)},
|
|
{"reported_iteration_to_1e-6", static_cast<double>(iteration1e6)},
|
|
{"reported_iteration_to_1e-8", static_cast<double>(iteration1e8)},
|
|
{"rhs_norm", solve.directResidual.rightHandSideNorm},
|
|
{"true_residual_norm", solve.directResidual.trueResidualNorm},
|
|
{"true_relative_residual", solve.directResidual.relativeResidual},
|
|
{"relative_correction_error", relativeCorrectionError},
|
|
{"true_residual_digits_per_jacobian_application", solve.trueResidualDigitsReducedPerJacobianApplication},
|
|
{"solve_seconds_maximum_rank", solve.solveSecondsMaximumRank},
|
|
{"jacobian_applications", static_cast<double>(solve.jacobian.applications)},
|
|
{"jacobian_application_seconds", solve.jacobian.totalSeconds},
|
|
{"jacobian_maximum_application_seconds", solve.jacobian.maximumSeconds},
|
|
{"preconditioner_applications", static_cast<double>(solve.inversePreconditioner.applications)},
|
|
{"preconditioner_application_seconds", solve.inversePreconditioner.totalSeconds},
|
|
{"preconditioner_maximum_application_seconds", solve.inversePreconditioner.maximumSeconds},
|
|
{"preconditioner_setups", static_cast<double>(solve.inversePreconditionerLifecycle.setups)},
|
|
{"preconditioner_setup_seconds_in_solver", solve.inversePreconditionerLifecycle.setupSeconds},
|
|
{"preconditioner_setup_seconds_maximum_rank", preconditionerSetupSeconds},
|
|
{"finite_element_setup_seconds", setupTimings.finiteElementSeconds},
|
|
{"lane_emden_calibration_seconds", setupTimings.laneEmdenCalibrationSeconds},
|
|
{"problem_construction_seconds", setupTimings.problemConstructionSeconds},
|
|
{"seed_projection_seconds", setupTimings.seedProjectionSeconds},
|
|
{"operator_preparation_seconds", setupTimings.operatorPreparationSeconds},
|
|
{"manufactured_rhs_seconds", setupTimings.manufacturedRightHandSideSeconds}
|
|
};
|
|
for (const auto &block : solve.directResidual.blocks) {
|
|
const std::string prefix = "residual_block." + block.stableId;
|
|
metrics[prefix + ".size"] = static_cast<double>(block.size);
|
|
metrics[prefix + ".descriptor_scale"] = block.descriptorScale;
|
|
metrics[prefix + ".rhs_norm"] = block.rightHandSideNorm;
|
|
metrics[prefix + ".true_norm"] = block.trueResidualNorm;
|
|
metrics[prefix + ".block_relative_residual"] = block.blockRelativeResidual;
|
|
metrics[prefix + ".scaled_rhs_norm"] = block.scaledRightHandSideNorm;
|
|
metrics[prefix + ".scaled_true_norm"] = block.scaledTrueResidualNorm;
|
|
metrics[prefix + ".global_relative_contribution"] = block.contributionToGlobalRelativeResidual;
|
|
metrics[prefix + ".fraction_global_squared_residual"] = block.fractionOfGlobalSquaredResidualNorm;
|
|
}
|
|
const double correctionErrorNorm = globalNorm(correctionError, communicator);
|
|
for (const auto &block : problem.GetManifest().valueBlocks()) {
|
|
const mfem::Vector exactBlock(
|
|
const_cast<mfem::real_t *>(exactCorrection.GetData()) + block.offset, block.size
|
|
);
|
|
const mfem::Vector errorBlock(correctionError.GetData() + block.offset, block.size);
|
|
const double exactBlockNorm = globalNorm(exactBlock, communicator);
|
|
const double errorBlockNorm = globalNorm(errorBlock, communicator);
|
|
const std::string prefix = "correction_block." + std::string(block.stableId);
|
|
metrics[prefix + ".size"] = static_cast<double>(block.size);
|
|
metrics[prefix + ".descriptor_scale"] = block.scale;
|
|
metrics[prefix + ".exact_norm"] = exactBlockNorm;
|
|
metrics[prefix + ".error_norm"] = errorBlockNorm;
|
|
metrics[prefix + ".block_relative_error"] =
|
|
errorBlockNorm / std::max(exactBlockNorm, std::numeric_limits<double>::min());
|
|
metrics[prefix + ".scaled_exact_norm"] = exactBlockNorm / block.scale;
|
|
metrics[prefix + ".scaled_error_norm"] = errorBlockNorm / block.scale;
|
|
metrics[prefix + ".fraction_global_squared_error"] =
|
|
correctionErrorNorm > 0.0
|
|
? errorBlockNorm * errorBlockNorm / (correctionErrorNorm * correctionErrorNorm)
|
|
: 0.0;
|
|
}
|
|
experiment::record_experiment_result(
|
|
"stellar_preconditioning_p10", candidate.name + "_linear_solve",
|
|
commonParameters(candidate, "manufactured_linear_solve", problem.StateSize()), std::move(metrics)
|
|
);
|
|
|
|
for (std::size_t index = 0; index < solve.reportedResidualHistory.size(); ++index) {
|
|
const auto &sample = solve.reportedResidualHistory[index];
|
|
experiment::record_experiment_result(
|
|
"stellar_preconditioning_p10", candidate.name + "_history_" + std::to_string(index),
|
|
commonParameters(candidate, "fgmres_residual_history", problem.StateSize()),
|
|
{{"history_sample", static_cast<double>(index)},
|
|
{"iteration", static_cast<double>(sample.iteration)},
|
|
{"reported_residual_norm", sample.reportedNorm},
|
|
{"reported_relative_residual", std::abs(sample.reportedNorm) / reportedInitial},
|
|
{"final_measurement", sample.final ? 1.0 : 0.0}}
|
|
);
|
|
}
|
|
|
|
instrumentedJacobian.ResetStatistics();
|
|
instrumentedPreconditioner.ResetStatistics();
|
|
solver::FixedRightPreconditionedOperator product(instrumentedJacobian, instrumentedPreconditioner);
|
|
ArnoldiProgressOperator progress(product, communicator, arnoldiDimension);
|
|
announce(communicator, "measuring " + candidate.name + " with 12-vector Arnoldi");
|
|
const solver::ArnoldiSpectralMeasurement spectrum = solver::measureArnoldiSpectrum(
|
|
progress, arnoldiDirection, communicator,
|
|
{.krylovDimension = arnoldiDimension,
|
|
.breakdownRelativeTolerance = 1.0e-13,
|
|
.ritzConvergenceRelativeTolerance = 1.0e-7,
|
|
.reorthogonalize = true}
|
|
);
|
|
REQUIRE(spectrum.achievedDimension > 0);
|
|
recordSpectrum(
|
|
candidate, spectrum, problem.StateSize(), preconditionerSetupSeconds, instrumentedJacobian.GetStatistics(),
|
|
instrumentedPreconditioner.GetStatistics()
|
|
);
|
|
|
|
int rank = 0;
|
|
MPI_Comm_rank(communicator, &rank);
|
|
if (rank == 0) {
|
|
std::cout << "[P10 full stellar] " << candidate.name << ": iterations=" << solve.outerIterations
|
|
<< ", converged=" << (solve.solverConverged ? "yes" : "no")
|
|
<< ", true residual=" << solve.directResidual.relativeResidual
|
|
<< ", correction error=" << relativeCorrectionError
|
|
<< ", projected condition=" << spectrum.projectedConditionProxy << '\n';
|
|
}
|
|
}
|
|
|
|
template <typename Problem>
|
|
void prepareAndMeasureDefault(
|
|
const CandidateDescription &candidate,
|
|
const Problem &problem,
|
|
const mfem::Vector &exactCorrection,
|
|
const mfem::Vector &rightHandSide,
|
|
const mfem::Vector &arnoldiDirection,
|
|
const SetupTimings &setupTimings,
|
|
const MPI_Comm communicator
|
|
) {
|
|
const Clock::time_point setupStart = Clock::now();
|
|
auto block = preconditioning::makePreconditioner(problem);
|
|
auto prepared = preconditioning::prepare(problem, std::move(block));
|
|
const double setupSeconds = maximumRankSeconds(setupStart, communicator);
|
|
measureCandidate(
|
|
candidate, prepared, setupSeconds, problem, exactCorrection, rightHandSide, arnoldiDirection, setupTimings,
|
|
communicator
|
|
);
|
|
}
|
|
|
|
template <
|
|
preconditioning::MaterialSurfaceFactorizationPolicy MaterialPolicy,
|
|
preconditioning::StellarStructureFactorizationPolicy StructurePolicy,
|
|
typename Problem,
|
|
backend::Registered GravityMassBackend = backend::Diagonal>
|
|
requires backend::Compatible<
|
|
GravityMassBackend,
|
|
preconditioning::GravityMassInverseCharacteristics>
|
|
void prepareAndMeasureComposed(
|
|
const CandidateDescription &candidate,
|
|
const Problem &problem,
|
|
const MaterialPolicy materialPolicy,
|
|
const StructurePolicy structurePolicy,
|
|
const preconditioning::MaterialSurfaceDiagonalOptions materialOptions,
|
|
const mfem::Vector &exactCorrection,
|
|
const mfem::Vector &rightHandSide,
|
|
const mfem::Vector &arnoldiDirection,
|
|
const SetupTimings &setupTimings,
|
|
const MPI_Comm communicator,
|
|
GravityMassBackend gravityMassBackend = {},
|
|
const int gravityAmgCycles = 1
|
|
) {
|
|
const Clock::time_point setupStart = Clock::now();
|
|
auto material = preconditioning::materialSurfaceBlock(
|
|
problem, backend::Diagonal{}, backend::Diagonal{}, materialPolicy, materialOptions
|
|
);
|
|
using FixedAMG = backend::HypreBoomerAMG<backend::FixedCycles>;
|
|
auto gravity = preconditioning::GravityFieldBlock(
|
|
std::move(gravityMassBackend), FixedAMG{backend::FixedCycles{.cycles = gravityAmgCycles}},
|
|
preconditioning::GravityApproximateLDU{}
|
|
);
|
|
auto structure =
|
|
preconditioning::stellarStructureBlock(problem, std::move(material), std::move(gravity), structurePolicy);
|
|
auto block = preconditioning::specificationBorderBlock(problem, std::move(structure), backend::DenseDirect{});
|
|
auto prepared = preconditioning::prepare(problem, std::move(block));
|
|
const double setupSeconds = maximumRankSeconds(setupStart, communicator);
|
|
measureCandidate(
|
|
candidate, prepared, setupSeconds, problem, exactCorrection, rightHandSide, arnoldiDirection, setupTimings,
|
|
communicator
|
|
);
|
|
}
|
|
|
|
template <
|
|
typename Preconditioner,
|
|
typename Problem>
|
|
[[nodiscard]] NewtonCandidateResult solveNewtonCandidate(
|
|
const CandidateDescription &candidate,
|
|
Preconditioner &inversePreconditioner,
|
|
const double preconditionerSetupSeconds,
|
|
const Problem &problem,
|
|
const mfem::Vector &baseResidual,
|
|
const mfem::Vector &rightHandSide,
|
|
const MPI_Comm communicator
|
|
) {
|
|
constexpr int maximumIterations = 48;
|
|
constexpr int restartDimension = 20;
|
|
|
|
const mfem::Operator &jacobian = problem.GetLinearizationOperator();
|
|
solver::InstrumentedOperator instrumentedJacobian(jacobian);
|
|
solver::InstrumentedPreconditioner instrumentedPreconditioner(inversePreconditioner);
|
|
solver::ResidualHistoryMonitor monitor;
|
|
mfem::FGMRESSolver krylov(communicator);
|
|
krylov.SetPreconditioner(instrumentedPreconditioner);
|
|
krylov.SetOperator(instrumentedJacobian);
|
|
krylov.SetMonitor(monitor);
|
|
krylov.SetRelTol(1.0e-8);
|
|
krylov.SetAbsTol(1.0e-12);
|
|
krylov.SetMaxIter(maximumIterations);
|
|
krylov.SetKDim(restartDimension);
|
|
krylov.SetPrintLevel(0);
|
|
|
|
mfem::Vector correction(problem.StateSize());
|
|
correction = 0.0;
|
|
announce(communicator, "solving the physical Newton system with " + candidate.name);
|
|
const Clock::time_point solveStart = Clock::now();
|
|
krylov.Mult(rightHandSide, correction);
|
|
const double localSolveSeconds = std::chrono::duration<double>(Clock::now() - solveStart).count();
|
|
|
|
const solver::LinearSolveMeasurement solve = solver::measureLinearSolve(
|
|
krylov, jacobian, rightHandSide, correction, problem.GetManifest().residualBlocks(),
|
|
instrumentedJacobian.GetStatistics(), instrumentedPreconditioner.GetStatistics(),
|
|
instrumentedPreconditioner.GetLifecycleStatistics(), monitor, localSolveSeconds, communicator
|
|
);
|
|
mfem::Vector linearAction(problem.EquationSize());
|
|
jacobian.Mult(correction, linearAction);
|
|
mfem::Vector predictedResidual(baseResidual);
|
|
predictedResidual += linearAction;
|
|
|
|
const double baseResidualNorm = globalNorm(baseResidual, communicator);
|
|
const double correctionNorm = globalNorm(correction, communicator);
|
|
const double predictedResidualNorm = globalNorm(predictedResidual, communicator);
|
|
REQUIRE(std::isfinite(solve.directResidual.relativeResidual));
|
|
REQUIRE(std::isfinite(correctionNorm));
|
|
REQUIRE(std::isfinite(predictedResidualNorm));
|
|
|
|
std::map<std::string, double> metrics{
|
|
{"maximum_iterations", static_cast<double>(maximumIterations)},
|
|
{"restart_dimension", static_cast<double>(restartDimension)},
|
|
{"solver_converged", solve.solverConverged ? 1.0 : 0.0},
|
|
{"outer_iterations", static_cast<double>(solve.outerIterations)},
|
|
{"reported_initial_residual_norm", solve.solverReportedInitialNorm},
|
|
{"reported_final_residual_norm", solve.solverReportedFinalNorm},
|
|
{"reported_residual_reduction", solve.solverReportedResidualReduction},
|
|
{"base_nonlinear_residual_norm", baseResidualNorm},
|
|
{"rhs_norm", solve.directResidual.rightHandSideNorm},
|
|
{"true_linear_residual_norm", solve.directResidual.trueResidualNorm},
|
|
{"true_linear_relative_residual", solve.directResidual.relativeResidual},
|
|
{"predicted_full_step_residual_norm", predictedResidualNorm},
|
|
{"predicted_full_step_relative_residual",
|
|
predictedResidualNorm / std::max(baseResidualNorm, std::numeric_limits<double>::min())},
|
|
{"correction_norm", correctionNorm},
|
|
{"solve_seconds_maximum_rank", solve.solveSecondsMaximumRank},
|
|
{"jacobian_applications", static_cast<double>(solve.jacobian.applications)},
|
|
{"jacobian_application_seconds", solve.jacobian.totalSeconds},
|
|
{"preconditioner_applications", static_cast<double>(solve.inversePreconditioner.applications)},
|
|
{"preconditioner_application_seconds", solve.inversePreconditioner.totalSeconds},
|
|
{"preconditioner_setup_seconds_maximum_rank", preconditionerSetupSeconds}
|
|
};
|
|
for (const auto &block : solve.directResidual.blocks) {
|
|
const std::string prefix = "linear_residual_block." + block.stableId;
|
|
metrics[prefix + ".rhs_norm"] = block.rightHandSideNorm;
|
|
metrics[prefix + ".true_norm"] = block.trueResidualNorm;
|
|
metrics[prefix + ".block_relative_residual"] = block.blockRelativeResidual;
|
|
metrics[prefix + ".global_relative_contribution"] = block.contributionToGlobalRelativeResidual;
|
|
}
|
|
for (const auto &block : problem.GetManifest().valueBlocks()) {
|
|
const mfem::Vector correctionBlock(correction.GetData() + block.offset, block.size);
|
|
const std::string prefix = "correction_block." + std::string(block.stableId);
|
|
metrics[prefix + ".norm"] = globalNorm(correctionBlock, communicator);
|
|
metrics[prefix + ".descriptor_scale"] = block.scale;
|
|
metrics[prefix + ".scaled_norm"] = metrics[prefix + ".norm"] / block.scale;
|
|
}
|
|
experiment::record_experiment_result(
|
|
"stellar_preconditioning_p10_newton", candidate.name + "_linear_solve",
|
|
newtonParameters(candidate, "physical_newton_linear_solve", problem.StateSize()), std::move(metrics)
|
|
);
|
|
|
|
const double reportedInitial = std::max(std::abs(krylov.GetInitialNorm()), 1.0e-300);
|
|
for (std::size_t index = 0; index < solve.reportedResidualHistory.size(); ++index) {
|
|
const auto &sample = solve.reportedResidualHistory[index];
|
|
experiment::record_experiment_result(
|
|
"stellar_preconditioning_p10_newton", candidate.name + "_history_" + std::to_string(index),
|
|
newtonParameters(candidate, "fgmres_residual_history", problem.StateSize()),
|
|
{{"history_sample", static_cast<double>(index)},
|
|
{"iteration", static_cast<double>(sample.iteration)},
|
|
{"reported_residual_norm", sample.reportedNorm},
|
|
{"reported_relative_residual", std::abs(sample.reportedNorm) / reportedInitial},
|
|
{"final_measurement", sample.final ? 1.0 : 0.0}}
|
|
);
|
|
}
|
|
|
|
return {.candidate = candidate, .correction = std::move(correction), .linearAction = std::move(linearAction)};
|
|
}
|
|
|
|
template <
|
|
preconditioning::StellarStructureFactorizationPolicy StructurePolicy,
|
|
typename Problem>
|
|
[[nodiscard]] NewtonCandidateResult prepareAndSolveNewtonComposed(
|
|
const CandidateDescription &candidate,
|
|
const Problem &problem,
|
|
const StructurePolicy structurePolicy,
|
|
const preconditioning::MaterialSurfaceDiagonalOptions materialOptions,
|
|
const mfem::Vector &baseResidual,
|
|
const mfem::Vector &rightHandSide,
|
|
const MPI_Comm communicator
|
|
) {
|
|
const Clock::time_point setupStart = Clock::now();
|
|
auto material = preconditioning::materialSurfaceBlock(
|
|
problem, backend::Diagonal{}, backend::Diagonal{}, preconditioning::SurfaceThenMaterialTriangular{},
|
|
materialOptions
|
|
);
|
|
using FixedAMG = backend::HypreBoomerAMG<backend::FixedCycles>;
|
|
auto gravity = preconditioning::GravityFieldBlock(
|
|
backend::Diagonal{}, FixedAMG{backend::FixedCycles{.cycles = 1}}, preconditioning::GravityApproximateLDU{}
|
|
);
|
|
auto structure =
|
|
preconditioning::stellarStructureBlock(problem, std::move(material), std::move(gravity), structurePolicy);
|
|
auto block = preconditioning::specificationBorderBlock(problem, std::move(structure), backend::DenseDirect{});
|
|
auto prepared = preconditioning::prepare(problem, std::move(block));
|
|
const double setupSeconds = maximumRankSeconds(setupStart, communicator);
|
|
return solveNewtonCandidate(
|
|
candidate, prepared, setupSeconds, problem, baseResidual, rightHandSide, communicator
|
|
);
|
|
}
|
|
} // namespace
|
|
|
|
TEST_CASE(
|
|
"Full Stellar P9 P10 Canonical Preconditioner Comparison",
|
|
"[preconditioning][diagnostics][experiment][spectrum][p9][p10][full_system]"
|
|
) {
|
|
using namespace mean_field;
|
|
|
|
constexpr int radialSampleCount = 4096;
|
|
const Clock::time_point finiteElementStart = Clock::now();
|
|
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();
|
|
int communicatorSize = 0;
|
|
MPI_Comm_size(communicator, &communicatorSize);
|
|
REQUIRE(communicatorSize == 1);
|
|
|
|
SetupTimings setupTimings;
|
|
setupTimings.finiteElementSeconds = maximumRankSeconds(finiteElementStart, communicator);
|
|
|
|
constexpr double stellarRadius = utils::RADIUS;
|
|
constexpr double targetMass = utils::MASS;
|
|
const Clock::time_point calibrationStart = Clock::now();
|
|
const seed::DimensionlessLaneEmdenSolution profile = seed::integrateLaneEmden(3.0, 10.0);
|
|
REQUIRE(profile.firstZeroCoordinate.has_value());
|
|
const double surfaceCoordinate = *profile.firstZeroCoordinate;
|
|
const double surfaceDerivative = profile.thetaDerivative(profile.thetaDerivative.Size() - 1);
|
|
const double dimensionlessMass = -surfaceCoordinate * surfaceCoordinate * surfaceDerivative;
|
|
REQUIRE(dimensionlessMass > 0.0);
|
|
const double massScale = targetMass / (4.0 * std::numbers::pi_v<double> * dimensionlessMass);
|
|
const double polytropicConstant = std::numbers::pi_v<double> * utils::G * std::pow(massScale, 2.0 / 3.0);
|
|
const double radialScale = stellarRadius / surfaceCoordinate;
|
|
const double centralDensity =
|
|
std::pow(polytropicConstant / (std::numbers::pi_v<double> * utils::G * radialScale * radialScale), 1.5);
|
|
setupTimings.laneEmdenCalibrationSeconds = maximumRankSeconds(calibrationStart, communicator);
|
|
|
|
const Clock::time_point constructionStart = Clock::now();
|
|
auto model = model::StellarModel(
|
|
eos::Polytrope({.n = 3.0, .K = polytropicConstant}),
|
|
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
|
|
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{targetMass}}),
|
|
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
|
|
);
|
|
auto problem = equilibrium::discretize(model, std::move(finiteElements));
|
|
setupTimings.problemConstructionSeconds = maximumRankSeconds(constructionStart, communicator);
|
|
|
|
announce(communicator, "projecting the n=3 Lane-Emden state");
|
|
const Clock::time_point projectionStart = Clock::now();
|
|
auto projected =
|
|
seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = radialSampleCount}));
|
|
setupTimings.seedProjectionSeconds = maximumRankSeconds(projectionStart, communicator);
|
|
|
|
announce(communicator, "preparing the canonical bordered stellar Jacobian");
|
|
const Clock::time_point preparationStart = Clock::now();
|
|
const auto preparation = problem.Prepare(projected.values, makeDependencies(), zeroRotation());
|
|
REQUIRE(preparation.assembledResidual);
|
|
setupTimings.operatorPreparationSeconds = maximumRankSeconds(preparationStart, communicator);
|
|
|
|
const mfem::Operator &jacobian = problem.GetLinearizationOperator();
|
|
const mfem::Vector exactCorrection =
|
|
blockBalancedDirection(problem.StateSize(), problem.GetManifest().valueBlocks(), 0.23, communicator);
|
|
mfem::Vector rightHandSide(problem.EquationSize());
|
|
const Clock::time_point rightHandSideStart = Clock::now();
|
|
jacobian.Mult(exactCorrection, rightHandSide);
|
|
setupTimings.manufacturedRightHandSideSeconds = maximumRankSeconds(rightHandSideStart, communicator);
|
|
REQUIRE(std::isfinite(globalNorm(rightHandSide, communicator)));
|
|
const mfem::Vector arnoldiDirection =
|
|
blockBalancedDirection(problem.EquationSize(), problem.GetManifest().residualBlocks(), 0.79, communicator);
|
|
|
|
solver::IdentityPreconditioner identity(problem.StateSize());
|
|
measureCandidate(
|
|
{.name = "identity",
|
|
.materialFactorization = "identity",
|
|
.surfaceCalibration = "none",
|
|
.stellarStructureFactorization = "identity",
|
|
.gravityFactorization = "identity",
|
|
.specificationBorder = "identity"},
|
|
identity, 0.0, problem, exactCorrection, rightHandSide, arnoldiDirection, setupTimings, communicator
|
|
);
|
|
|
|
prepareAndMeasureDefault(
|
|
{.name = "current_default",
|
|
.materialFactorization = "surface_then_material_triangular",
|
|
.surfaceCalibration = "none",
|
|
.stellarStructureFactorization = "independent_subsystems",
|
|
.gravityFactorization = "approximate_ldu",
|
|
.specificationBorder = "compiled_dense_schur"},
|
|
problem, exactCorrection, rightHandSide, arnoldiDirection, setupTimings, communicator
|
|
);
|
|
|
|
// P9 found that fitting the surface Riesz scale to A_qq gives the same
|
|
// measured material-surface numerics as fitting the approximate Schur
|
|
// complement while reducing calibration setup by roughly forty percent.
|
|
// Keep both successful material factorizations explicit here: calibrated
|
|
// surface-first is cheaper, while calibrated material LDU gave the best
|
|
// isolated residual.
|
|
constexpr preconditioning::MaterialSurfaceDiagonalOptions surfaceJacobianCalibratedOptions{
|
|
.surfaceCalibration = {
|
|
.target = preconditioning::SurfaceRieszCalibrationTarget::surface_jacobian, .probeCount = 4
|
|
}
|
|
};
|
|
|
|
prepareAndMeasureComposed(
|
|
{.name = "surface_then_material_aqq_calibrated_independent",
|
|
.materialFactorization = "surface_then_material_triangular",
|
|
.surfaceCalibration = "surface_jacobian_4_probes",
|
|
.stellarStructureFactorization = "independent_subsystems",
|
|
.gravityFactorization = "approximate_ldu",
|
|
.specificationBorder = "compiled_dense_schur"},
|
|
problem, preconditioning::SurfaceThenMaterialTriangular{}, preconditioning::IndependentStellarSubsystems{},
|
|
surfaceJacobianCalibratedOptions, exactCorrection, rightHandSide, arnoldiDirection, setupTimings, communicator
|
|
);
|
|
prepareAndMeasureComposed(
|
|
{.name = "material_ldu_aqq_calibrated_independent",
|
|
.materialFactorization = "approximate_material_surface_ldu",
|
|
.surfaceCalibration = "surface_jacobian_4_probes",
|
|
.stellarStructureFactorization = "independent_subsystems",
|
|
.gravityFactorization = "approximate_ldu",
|
|
.specificationBorder = "compiled_dense_schur"},
|
|
problem, preconditioning::ApproximateMaterialSurfaceLDU{}, preconditioning::IndependentStellarSubsystems{},
|
|
surfaceJacobianCalibratedOptions, exactCorrection, rightHandSide, arnoldiDirection, setupTimings, communicator
|
|
);
|
|
prepareAndMeasureComposed(
|
|
{.name = "surface_then_material_aqq_calibrated_then_gravity",
|
|
.materialFactorization = "surface_then_material_triangular",
|
|
.surfaceCalibration = "surface_jacobian_4_probes",
|
|
.stellarStructureFactorization = "material_then_gravity_triangular",
|
|
.gravityFactorization = "approximate_ldu",
|
|
.specificationBorder = "compiled_dense_schur"},
|
|
problem, preconditioning::SurfaceThenMaterialTriangular{}, preconditioning::MaterialThenGravityTriangular{},
|
|
surfaceJacobianCalibratedOptions, exactCorrection, rightHandSide, arnoldiDirection, setupTimings, communicator
|
|
);
|
|
prepareAndMeasureComposed(
|
|
{.name = "gravity_then_surface_then_material_aqq_calibrated",
|
|
.materialFactorization = "surface_then_material_triangular",
|
|
.surfaceCalibration = "surface_jacobian_4_probes",
|
|
.stellarStructureFactorization = "gravity_then_material_triangular",
|
|
.gravityFactorization = "approximate_ldu",
|
|
.specificationBorder = "compiled_dense_schur"},
|
|
problem, preconditioning::SurfaceThenMaterialTriangular{}, preconditioning::GravityThenMaterialTriangular{},
|
|
surfaceJacobianCalibratedOptions, exactCorrection, rightHandSide, arnoldiDirection, setupTimings, communicator
|
|
);
|
|
prepareAndMeasureComposed(
|
|
{.name = "surface_then_material_aqq_calibrated_stellar_approximate_ldu",
|
|
.materialFactorization = "surface_then_material_triangular",
|
|
.surfaceCalibration = "surface_jacobian_4_probes",
|
|
.stellarStructureFactorization = "approximate_stellar_block_ldu",
|
|
.gravityFactorization = "approximate_ldu",
|
|
.specificationBorder = "compiled_dense_schur"},
|
|
problem, preconditioning::SurfaceThenMaterialTriangular{}, preconditioning::ApproximateStellarBlockLDU{},
|
|
surfaceJacobianCalibratedOptions, exactCorrection, rightHandSide, arnoldiDirection, setupTimings, communicator
|
|
);
|
|
prepareAndMeasureComposed(
|
|
{.name = "material_ldu_aqq_calibrated_stellar_approximate_ldu",
|
|
.materialFactorization = "approximate_material_surface_ldu",
|
|
.surfaceCalibration = "surface_jacobian_4_probes",
|
|
.stellarStructureFactorization = "approximate_stellar_block_ldu",
|
|
.gravityFactorization = "approximate_ldu",
|
|
.specificationBorder = "compiled_dense_schur"},
|
|
problem, preconditioning::ApproximateMaterialSurfaceLDU{}, preconditioning::ApproximateStellarBlockLDU{},
|
|
surfaceJacobianCalibratedOptions, exactCorrection, rightHandSide, arnoldiDirection, setupTimings, communicator
|
|
);
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Full Stellar P10 Gravity Backend Finalists",
|
|
"[preconditioning][diagnostics][experiment][p10][full_system][gravity_backend_finalists]"
|
|
) {
|
|
using namespace mean_field;
|
|
|
|
constexpr int radialSampleCount = 4096;
|
|
const Clock::time_point finiteElementStart = Clock::now();
|
|
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();
|
|
int communicatorSize = 0;
|
|
MPI_Comm_size(communicator, &communicatorSize);
|
|
REQUIRE(communicatorSize == 1);
|
|
|
|
SetupTimings setupTimings;
|
|
setupTimings.finiteElementSeconds = maximumRankSeconds(finiteElementStart, communicator);
|
|
|
|
constexpr double stellarRadius = utils::RADIUS;
|
|
constexpr double targetMass = utils::MASS;
|
|
const Clock::time_point calibrationStart = Clock::now();
|
|
const seed::DimensionlessLaneEmdenSolution profile = seed::integrateLaneEmden(3.0, 10.0);
|
|
REQUIRE(profile.firstZeroCoordinate.has_value());
|
|
const double surfaceCoordinate = *profile.firstZeroCoordinate;
|
|
const double surfaceDerivative = profile.thetaDerivative(profile.thetaDerivative.Size() - 1);
|
|
const double dimensionlessMass = -surfaceCoordinate * surfaceCoordinate * surfaceDerivative;
|
|
REQUIRE(dimensionlessMass > 0.0);
|
|
const double massScale = targetMass / (4.0 * std::numbers::pi_v<double> * dimensionlessMass);
|
|
const double polytropicConstant = std::numbers::pi_v<double> * utils::G * std::pow(massScale, 2.0 / 3.0);
|
|
const double radialScale = stellarRadius / surfaceCoordinate;
|
|
const double centralDensity =
|
|
std::pow(polytropicConstant / (std::numbers::pi_v<double> * utils::G * radialScale * radialScale), 1.5);
|
|
setupTimings.laneEmdenCalibrationSeconds = maximumRankSeconds(calibrationStart, communicator);
|
|
|
|
const Clock::time_point constructionStart = Clock::now();
|
|
auto model = model::StellarModel(
|
|
eos::Polytrope({.n = 3.0, .K = polytropicConstant}),
|
|
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
|
|
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{targetMass}}),
|
|
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
|
|
);
|
|
auto problem = equilibrium::discretize(model, std::move(finiteElements));
|
|
setupTimings.problemConstructionSeconds = maximumRankSeconds(constructionStart, communicator);
|
|
|
|
announce(communicator, "projecting the n=3 Lane-Emden state for gravity backend finalists");
|
|
const Clock::time_point projectionStart = Clock::now();
|
|
auto projected =
|
|
seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = radialSampleCount}));
|
|
setupTimings.seedProjectionSeconds = maximumRankSeconds(projectionStart, communicator);
|
|
|
|
const Clock::time_point preparationStart = Clock::now();
|
|
const auto preparation = problem.Prepare(projected.values, makeDependencies(), zeroRotation());
|
|
REQUIRE(preparation.assembledResidual);
|
|
setupTimings.operatorPreparationSeconds = maximumRankSeconds(preparationStart, communicator);
|
|
|
|
const mfem::Operator &jacobian = problem.GetLinearizationOperator();
|
|
const mfem::Vector exactCorrection =
|
|
blockBalancedDirection(problem.StateSize(), problem.GetManifest().valueBlocks(), 0.23, communicator);
|
|
mfem::Vector rightHandSide(problem.EquationSize());
|
|
const Clock::time_point rightHandSideStart = Clock::now();
|
|
jacobian.Mult(exactCorrection, rightHandSide);
|
|
setupTimings.manufacturedRightHandSideSeconds = maximumRankSeconds(rightHandSideStart, communicator);
|
|
const mfem::Vector arnoldiDirection =
|
|
blockBalancedDirection(problem.EquationSize(), problem.GetManifest().residualBlocks(), 0.79, communicator);
|
|
constexpr preconditioning::MaterialSurfaceDiagonalOptions materialOptions{};
|
|
|
|
prepareAndMeasureComposed(
|
|
{.name = "current_structure_diagonal_mass_amg2",
|
|
.materialFactorization = "surface_then_material_triangular",
|
|
.surfaceCalibration = "none",
|
|
.stellarStructureFactorization = "independent_subsystems",
|
|
.gravityFactorization = "approximate_ldu_diagonal_mass_amg2",
|
|
.specificationBorder = "compiled_dense_schur"},
|
|
problem, preconditioning::SurfaceThenMaterialTriangular{}, preconditioning::IndependentStellarSubsystems{},
|
|
materialOptions, exactCorrection, rightHandSide, arnoldiDirection, setupTimings, communicator,
|
|
backend::Diagonal{}, 2
|
|
);
|
|
prepareAndMeasureComposed(
|
|
{.name = "current_structure_chebyshev3_mass_amg2",
|
|
.materialFactorization = "surface_then_material_triangular",
|
|
.surfaceCalibration = "none",
|
|
.stellarStructureFactorization = "independent_subsystems",
|
|
.gravityFactorization = "approximate_ldu_chebyshev3_mass_amg2",
|
|
.specificationBorder = "compiled_dense_schur"},
|
|
problem, preconditioning::SurfaceThenMaterialTriangular{}, preconditioning::IndependentStellarSubsystems{},
|
|
materialOptions, exactCorrection, rightHandSide, arnoldiDirection, setupTimings, communicator,
|
|
backend::MatrixFreeChebyshev{.order = 3, .powerIterations = 20}, 2
|
|
);
|
|
prepareAndMeasureComposed(
|
|
{.name = "current_structure_chebyshev4_mass_amg3",
|
|
.materialFactorization = "surface_then_material_triangular",
|
|
.surfaceCalibration = "none",
|
|
.stellarStructureFactorization = "independent_subsystems",
|
|
.gravityFactorization = "approximate_ldu_chebyshev4_mass_amg3",
|
|
.specificationBorder = "compiled_dense_schur"},
|
|
problem, preconditioning::SurfaceThenMaterialTriangular{}, preconditioning::IndependentStellarSubsystems{},
|
|
materialOptions, exactCorrection, rightHandSide, arnoldiDirection, setupTimings, communicator,
|
|
backend::MatrixFreeChebyshev{.order = 4, .powerIterations = 20}, 3
|
|
);
|
|
prepareAndMeasureComposed(
|
|
{.name = "current_structure_chebyshev5_mass_amg3",
|
|
.materialFactorization = "surface_then_material_triangular",
|
|
.surfaceCalibration = "none",
|
|
.stellarStructureFactorization = "independent_subsystems",
|
|
.gravityFactorization = "approximate_ldu_chebyshev5_mass_amg3",
|
|
.specificationBorder = "compiled_dense_schur"},
|
|
problem, preconditioning::SurfaceThenMaterialTriangular{}, preconditioning::IndependentStellarSubsystems{},
|
|
materialOptions, exactCorrection, rightHandSide, arnoldiDirection, setupTimings, communicator,
|
|
backend::MatrixFreeChebyshev{.order = 5, .powerIterations = 20}, 3
|
|
);
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Full Stellar Physical Newton Right Hand Side And Damped Trial States",
|
|
"[preconditioning][diagnostics][experiment][p10][full_system][physical_newton_rhs]"
|
|
) {
|
|
using namespace mean_field;
|
|
|
|
constexpr int radialSampleCount = 4096;
|
|
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();
|
|
int communicatorSize = 0;
|
|
MPI_Comm_size(communicator, &communicatorSize);
|
|
REQUIRE(communicatorSize == 1);
|
|
|
|
constexpr double stellarRadius = utils::RADIUS;
|
|
constexpr double targetMass = utils::MASS;
|
|
const seed::DimensionlessLaneEmdenSolution profile = seed::integrateLaneEmden(3.0, 10.0);
|
|
REQUIRE(profile.firstZeroCoordinate.has_value());
|
|
const double surfaceCoordinate = *profile.firstZeroCoordinate;
|
|
const double surfaceDerivative = profile.thetaDerivative(profile.thetaDerivative.Size() - 1);
|
|
const double dimensionlessMass = -surfaceCoordinate * surfaceCoordinate * surfaceDerivative;
|
|
REQUIRE(dimensionlessMass > 0.0);
|
|
const double massScale = targetMass / (4.0 * std::numbers::pi_v<double> * dimensionlessMass);
|
|
const double polytropicConstant = std::numbers::pi_v<double> * utils::G * std::pow(massScale, 2.0 / 3.0);
|
|
const double radialScale = stellarRadius / surfaceCoordinate;
|
|
const double centralDensity =
|
|
std::pow(polytropicConstant / (std::numbers::pi_v<double> * utils::G * radialScale * radialScale), 1.5);
|
|
|
|
auto model = model::StellarModel(
|
|
eos::Polytrope({.n = 3.0, .K = polytropicConstant}),
|
|
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
|
|
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{targetMass}}),
|
|
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
|
|
);
|
|
auto problem = equilibrium::discretize(model, std::move(finiteElements));
|
|
auto projected =
|
|
seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = radialSampleCount}));
|
|
auto dependencies = makeDependencies();
|
|
const auto rotation = zeroRotation();
|
|
const auto preparation = problem.Prepare(projected.values, dependencies, rotation);
|
|
REQUIRE(preparation.assembledResidual);
|
|
|
|
mfem::Vector baseResidual;
|
|
problem.BuildResidual(baseResidual);
|
|
const double baseResidualNorm = globalNorm(baseResidual, communicator);
|
|
REQUIRE(std::isfinite(baseResidualNorm));
|
|
REQUIRE(baseResidualNorm > 0.0);
|
|
mfem::Vector rightHandSide(baseResidual);
|
|
rightHandSide *= -1.0;
|
|
|
|
constexpr preconditioning::MaterialSurfaceDiagonalOptions uncalibratedMaterialOptions{};
|
|
constexpr preconditioning::MaterialSurfaceDiagonalOptions rightCalibratedMaterialOptions{
|
|
.surfaceCalibration = {
|
|
.target = preconditioning::SurfaceRieszCalibrationTarget::surface_jacobian,
|
|
.probeCount = 4,
|
|
.objective = preconditioning::SurfaceRieszCalibrationObjective::right_preconditioned_action
|
|
}
|
|
};
|
|
|
|
std::vector<NewtonCandidateResult> candidates;
|
|
candidates.reserve(4);
|
|
solver::IdentityPreconditioner identity(problem.StateSize());
|
|
candidates.push_back(solveNewtonCandidate(
|
|
{.name = "identity",
|
|
.materialFactorization = "identity",
|
|
.surfaceCalibration = "none",
|
|
.stellarStructureFactorization = "identity",
|
|
.gravityFactorization = "identity",
|
|
.specificationBorder = "identity"},
|
|
identity, 0.0, problem, baseResidual, rightHandSide, communicator
|
|
));
|
|
candidates.push_back(prepareAndSolveNewtonComposed(
|
|
{.name = "current_default",
|
|
.materialFactorization = "surface_then_material_triangular",
|
|
.surfaceCalibration = "none",
|
|
.stellarStructureFactorization = "independent_subsystems",
|
|
.gravityFactorization = "approximate_ldu",
|
|
.specificationBorder = "compiled_dense_schur"},
|
|
problem, preconditioning::IndependentStellarSubsystems{}, uncalibratedMaterialOptions, baseResidual,
|
|
rightHandSide, communicator
|
|
));
|
|
candidates.push_back(prepareAndSolveNewtonComposed(
|
|
{.name = "surface_then_material_uncalibrated_then_gravity",
|
|
.materialFactorization = "surface_then_material_triangular",
|
|
.surfaceCalibration = "none",
|
|
.stellarStructureFactorization = "material_then_gravity_triangular",
|
|
.gravityFactorization = "approximate_ldu",
|
|
.specificationBorder = "compiled_dense_schur"},
|
|
problem, preconditioning::MaterialThenGravityTriangular{}, uncalibratedMaterialOptions, baseResidual,
|
|
rightHandSide, communicator
|
|
));
|
|
candidates.push_back(prepareAndSolveNewtonComposed(
|
|
{.name = "surface_then_material_right_calibrated_then_gravity",
|
|
.materialFactorization = "surface_then_material_triangular",
|
|
.surfaceCalibration = "surface_jacobian_right_action_4_probes",
|
|
.stellarStructureFactorization = "material_then_gravity_triangular",
|
|
.gravityFactorization = "approximate_ldu",
|
|
.specificationBorder = "compiled_dense_schur"},
|
|
problem, preconditioning::MaterialThenGravityTriangular{}, rightCalibratedMaterialOptions, baseResidual,
|
|
rightHandSide, communicator
|
|
));
|
|
|
|
// All four corrections above were obtained while the problem remained at
|
|
// the identical projected Lane-Emden base point. Only now do we mutate the
|
|
// prepared state to measure the nonlinear quality of each correction.
|
|
constexpr std::array<double, 7> dampingFactors{1.0, 0.5, 0.25, 0.125, 0.0625, 0.03125, 0.015625};
|
|
const auto valueBlocks = problem.GetManifest().valueBlocks();
|
|
const auto residualBlocks = problem.GetManifest().residualBlocks();
|
|
const auto &surfaceBlock = findBlock(valueBlocks, "surface_deformation");
|
|
const auto &densityBlock = findBlock(valueBlocks, "density");
|
|
const auto &physical = problem.GetPreparedOperator().GetPhysicalOperator();
|
|
const auto &domainDeformation = physical.GetDomainDeformation();
|
|
mfem::Vector volumeDisplacement(domainDeformation.volumeDisplacementSize());
|
|
|
|
for (const NewtonCandidateResult &candidate : candidates) {
|
|
for (const double alpha : dampingFactors) {
|
|
incrementStateRevisions(dependencies);
|
|
mfem::Vector trialState(projected.values);
|
|
trialState.Add(alpha, candidate.correction);
|
|
|
|
int localStateIsFinite = 1;
|
|
for (int index = 0; index < trialState.Size(); ++index) {
|
|
if (!std::isfinite(trialState(index))) {
|
|
localStateIsFinite = 0;
|
|
}
|
|
}
|
|
int stateIsFinite = 0;
|
|
MPI_Allreduce(&localStateIsFinite, &stateIsFinite, 1, MPI_INT, MPI_MIN, communicator);
|
|
|
|
const mfem::Vector density(trialState.GetData() + densityBlock.offset, densityBlock.size);
|
|
double localMinimumDensity = std::numeric_limits<double>::infinity();
|
|
for (int index = 0; index < density.Size(); ++index) {
|
|
localMinimumDensity = std::min(localMinimumDensity, density(index));
|
|
}
|
|
double minimumDensity = 0.0;
|
|
MPI_Allreduce(&localMinimumDensity, &minimumDensity, 1, MPI_DOUBLE, MPI_MIN, communicator);
|
|
|
|
const mfem::Vector surfaceParameters(trialState.GetData() + surfaceBlock.offset, surfaceBlock.size);
|
|
domainDeformation.buildVolumeDisplacement(surfaceParameters, volumeDisplacement);
|
|
const deformation::DomainDeformationGeometryReport geometry =
|
|
domainDeformation.inspectMappedGeometry(volumeDisplacement);
|
|
const bool geometryIsValid = geometry.isOrientationPreserving();
|
|
const bool densityIsValid = std::isfinite(minimumDensity) && minimumDensity >= 0.0;
|
|
const bool trialIsValid = stateIsFinite != 0 && geometryIsValid && densityIsValid;
|
|
|
|
mfem::Vector predictedResidual(baseResidual);
|
|
predictedResidual.Add(alpha, candidate.linearAction);
|
|
const double predictedNorm = globalNorm(predictedResidual, communicator);
|
|
std::map<std::string, double> metrics{
|
|
{"alpha", alpha},
|
|
{"dependency_revision", static_cast<double>(dependencies.density.revision)},
|
|
{"state_is_finite", stateIsFinite != 0 ? 1.0 : 0.0},
|
|
{"density_is_nonnegative", densityIsValid ? 1.0 : 0.0},
|
|
{"minimum_density_dof", minimumDensity},
|
|
{"geometry_is_orientation_preserving", geometryIsValid ? 1.0 : 0.0},
|
|
{"minimum_mapping_jacobian_determinant", geometry.minimumJacobianDeterminant},
|
|
{"trial_is_valid", trialIsValid ? 1.0 : 0.0},
|
|
{"base_residual_norm", baseResidualNorm},
|
|
{"correction_norm", globalNorm(candidate.correction, communicator)},
|
|
{"damped_correction_norm", alpha * globalNorm(candidate.correction, communicator)},
|
|
{"predicted_residual_norm", predictedNorm},
|
|
{"predicted_relative_residual", predictedNorm / baseResidualNorm},
|
|
{"predicted_fractional_reduction", 1.0 - predictedNorm / baseResidualNorm}
|
|
};
|
|
|
|
auto parameters =
|
|
newtonParameters(candidate.candidate, "damped_physical_newton_trial", problem.StateSize());
|
|
parameters["trial_status"] = trialIsValid ? "prevalidated" : "rejected_before_residual_evaluation";
|
|
if (!trialIsValid) {
|
|
experiment::record_experiment_result(
|
|
"stellar_preconditioning_p10_newton", candidate.candidate.name + "_alpha_" + std::to_string(alpha),
|
|
std::move(parameters), std::move(metrics)
|
|
);
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
problem.Prepare(trialState, dependencies, rotation);
|
|
mfem::Vector actualResidual;
|
|
problem.BuildResidual(actualResidual);
|
|
const double actualNorm = globalNorm(actualResidual, communicator);
|
|
mfem::Vector nonlinearRemainder(actualResidual);
|
|
nonlinearRemainder -= predictedResidual;
|
|
const double nonlinearRemainderNorm = globalNorm(nonlinearRemainder, communicator);
|
|
mfem::Vector residualDeparture(actualResidual);
|
|
residualDeparture -= baseResidual;
|
|
const double residualDepartureNorm = globalNorm(residualDeparture, communicator);
|
|
|
|
metrics["residual_evaluated"] = 1.0;
|
|
metrics["actual_residual_norm"] = actualNorm;
|
|
metrics["actual_relative_residual"] = actualNorm / baseResidualNorm;
|
|
metrics["actual_fractional_reduction"] = 1.0 - actualNorm / baseResidualNorm;
|
|
metrics["nonlinear_remainder_norm"] = nonlinearRemainderNorm;
|
|
metrics["relative_nonlinear_remainder"] =
|
|
nonlinearRemainderNorm / std::max(residualDepartureNorm, std::numeric_limits<double>::min());
|
|
metrics["actual_to_predicted_norm_ratio"] =
|
|
actualNorm / std::max(predictedNorm, std::numeric_limits<double>::min());
|
|
|
|
for (const auto &block : residualBlocks) {
|
|
const mfem::Vector baseBlock(baseResidual.GetData() + block.offset, block.size);
|
|
const mfem::Vector predictedBlock(predictedResidual.GetData() + block.offset, block.size);
|
|
const mfem::Vector actualBlock(actualResidual.GetData() + block.offset, block.size);
|
|
const mfem::Vector remainderBlock(nonlinearRemainder.GetData() + block.offset, block.size);
|
|
const double baseBlockNorm = globalNorm(baseBlock, communicator);
|
|
const std::string prefix = "residual_block." + std::string(block.stableId);
|
|
metrics[prefix + ".base_norm"] = baseBlockNorm;
|
|
metrics[prefix + ".predicted_norm"] = globalNorm(predictedBlock, communicator);
|
|
metrics[prefix + ".actual_norm"] = globalNorm(actualBlock, communicator);
|
|
metrics[prefix + ".actual_ratio"] =
|
|
metrics[prefix + ".actual_norm"] / std::max(baseBlockNorm, std::numeric_limits<double>::min());
|
|
metrics[prefix + ".remainder_norm"] = globalNorm(remainderBlock, communicator);
|
|
}
|
|
parameters["trial_status"] = "evaluated";
|
|
} catch (const std::exception &) {
|
|
metrics["residual_evaluated"] = 0.0;
|
|
parameters["trial_status"] = "residual_evaluation_threw";
|
|
}
|
|
|
|
experiment::record_experiment_result(
|
|
"stellar_preconditioning_p10_newton", candidate.candidate.name + "_alpha_" + std::to_string(alpha),
|
|
std::move(parameters), std::move(metrics)
|
|
);
|
|
}
|
|
}
|
|
}
|