perf(jacobian-action): major updates to jacobian action application by removing redudant quadrature work. ~5x increase in speed

This commit is contained in:
2026-09-02 17:01:50 -04:00
parent 85500fef3b
commit 25510008dd
74 changed files with 8967 additions and 814 deletions

View File

@@ -147,3 +147,42 @@ The executable needs the same dependencies, generated module mapping, and
configuration registration as the existing Catch2 test executable. Add
`experiment_main.cpp` and `gravity_accuracy_budget.cpp` as a second executable
next to that target; do not add them to the ordinary test executable.
## P0 preconditioning baseline
The P0 diagnostic establishes the unpreconditioned reference for the complete,
central-density-closed `n = 3` stellar equilibrium Jacobian. It uses an identity
inverse preconditioner with FGMRES, recomputes the true residual independently,
records every residual block, and counts and times Jacobian and preconditioner
applications. A separate fixed-operator Arnoldi measurement acts explicitly on
the right-preconditioned product `J M^-1`. Its singular-value ratio is a
projected Krylov-space condition proxy, not the condition number of the full
Jacobian. The same output records Ritz values, clustering about one,
nonnormality, and the real extent of the projected field of values.
The extended baseline preserves the fixed 40-iteration FGMRES budget used by
the original P0 run and increases the Arnoldi dimension from 12 to 48. It writes
the complete reported FGMRES residual history, block-relative and
manifest-scaled final residuals, the fraction of the squared residual in each
physics block, timings for construction/projection/preparation/direct-residual
measurement, and separate Arnoldi operator and orthogonalization timings. Live
progress messages delimit every expensive phase and report every fourth
Arnoldi application. The CSV records whether it came from a Debug or Release
build.
Run the focused synthetic verification tests with:
```text
./cmake-build-debug-homebrew/tests "[preconditioning][diagnostics][unit]"
```
Run the performance and spectral measurement separately with:
```text
mpirun -np 1 ./cmake-build-release-homebrew/experiments \
--experiment-output preconditioning_p0_identity_extended.csv \
--catch2 "[preconditioning][diagnostics][baseline]"
```
Set `MEANFIELD_SINGLE_JACOBIAN_BENCHMARK=1` to stop after the initial prepared
Jacobian timing instead of running FGMRES and Arnoldi.

View File

@@ -679,6 +679,93 @@ TEST_CASE(
null_space::report_progress(communicator, "coupled reduced surface-mode probe complete; writing CSV output");
}
TEST_CASE(
"Fixed Central Density Phase Couples To The N3 Homology Tangent",
"[null_space][homology][central_density][phase]"
) {
mean_field::utils::Args args = test_utils::setup_args();
null_space::N3Equilibrium fixture(std::move(args));
const MPI_Comm communicator = fixture.fem().mesh->GetComm();
const std::vector<GaugeMode> modes = make_gauge_modes(fixture);
const auto homology = std::ranges::find_if(modes, [](const GaugeMode &mode) { return mode.family == "homology"; });
REQUIRE(homology != modes.end());
const auto &layout = fixture.stellar_operator().GetLayout();
const mfem::Vector enthalpy = null_space::const_value_view(fixture.state(), layout, null_space::enthalpyValue);
const mfem::Vector enthalpyDirection =
null_space::const_value_view(homology->direction, layout, null_space::enthalpyValue);
const mean_field::field::FieldDofMap enthalpyMap =
mean_field::field::make_field_dof_map<mean_field::field::Enthalpy, null_space::DomainSchema>(
*fixture.fem().enthalpyFes
);
mfem::Vector origin(fixture.fem().mesh->SpaceDimension());
origin = 0.0;
mean_field::field::FieldPointDofMap centerDof =
mean_field::field::make_field_point_dof_map<mean_field::field::Enthalpy>(
*fixture.fem().enthalpyFes, enthalpyMap, origin, 1.0e-12
);
double localCentralEnthalpy = 0.0;
for (const int reducedDof : centerDof.reduced_dofs()) {
localCentralEnthalpy += enthalpy(reducedDof);
}
double centralEnthalpy = 0.0;
MPI_Allreduce(&localCentralEnthalpy, &centralEnthalpy, 1, MPI_DOUBLE, MPI_SUM, communicator);
REQUIRE(std::isfinite(centralEnthalpy));
REQUIRE(centralEnthalpy > 0.0);
const auto &equationOfState = fixture.model().equationOfState();
const mean_field::eos::DensityValue targetDensity = mean_field::eos::evaluate<mean_field::eos::quantity::Density>(
equationOfState, mean_field::eos::SpecificEnthalpyValue{centralEnthalpy}
);
const mean_field::models::CompiledFixedCentralDensity compiled =
mean_field::models::compileConstraint(mean_field::models::FixedCentralDensity{targetDensity}, equationOfState);
mean_field::operators::PreparedCentralDensityConstraint phase(std::move(centerDof), communicator);
phase.Prepare(compiled, enthalpy, 0.0, {.enthalpy = {.identity = 3251, .revision = 1}});
mfem::Vector enthalpyAction(enthalpy.Size());
mfem::Vector phaseAction(1);
enthalpyAction = 0.0;
phaseAction = 0.0;
phase.ApplyJacobian(
{.enthalpyVariation = enthalpyDirection, .borderVariation = 0.0},
{.enthalpyAction = enthalpyAction, .phaseAction = phaseAction}
);
const double couplingScale = std::max(1.0, std::abs(compiled.targetEnthalpy().value()));
const double enthalpyDirectionNorm = null_space::global_norm(enthalpyDirection, communicator);
const double homologyDirectionNorm = null_space::global_norm(homology->direction, communicator);
const double absolutePhaseCoupling = std::abs(phaseAction(0));
INFO("Central enthalpy = " << centralEnthalpy);
INFO("N3 homology phase coupling = " << phaseAction(0));
REQUIRE(std::isfinite(phaseAction(0)));
REQUIRE(enthalpyDirectionNorm > 0.0);
REQUIRE(homologyDirectionNorm > 0.0);
CHECK(absolutePhaseCoupling > 100.0 * std::numeric_limits<double>::epsilon() * couplingScale);
int rank = 0;
MPI_Comm_rank(communicator, &rank);
if (rank == 0) {
experiment::record_experiment_result(
"fixed_central_density_homology_coupling", "n3_homology",
{{"mesh_file", test_utils::setup_args().mesh_file},
{"local_state_dofs", std::to_string(fixture.stellar_operator().Width())}},
{{"target_density", compiled.targetDensity().value()},
{"target_enthalpy", compiled.targetEnthalpy().value()},
{"central_enthalpy", centralEnthalpy},
{"homology_phase_action", phaseAction(0)},
{"absolute_phase_coupling", absolutePhaseCoupling},
{"target_scaled_phase_coupling", absolutePhaseCoupling / couplingScale},
{"enthalpy_direction_norm", enthalpyDirectionNorm},
{"enthalpy_normalized_phase_coupling", absolutePhaseCoupling / enthalpyDirectionNorm},
{"homology_direction_norm", homologyDirectionNorm},
{"state_normalized_phase_coupling", absolutePhaseCoupling / homologyDirectionNorm}}
);
}
}
TEST_CASE(
"N3 Homology Mass Cancellation At The Registered Polynomial Order",
"[null_space][homology][mass_normalization][convergence][p_refinement]"

View File

@@ -0,0 +1,455 @@
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <map>
#include <numbers>
#include <ranges>
#include <span>
#include <string>
#include <utility>
#include <vector>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
#include <mpi.h>
import mean_field;
import test_helpers;
import experiment;
namespace {
using Clock = std::chrono::steady_clock;
[[nodiscard]] const char *build_configuration() noexcept {
#ifdef NDEBUG
return "release";
#else
return "debug";
#endif
}
[[nodiscard]] double maximum_rank_seconds(
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;
}
void announce(
const MPI_Comm communicator,
const std::string &message
) {
int rank{0};
MPI_Comm_rank(communicator, &rank);
if (rank == 0) {
std::cout << message << std::endl;
}
}
class ArnoldiProgressOperator final : public mfem::Operator {
public:
ArnoldiProgressOperator(
const mfem::Operator &operation,
const MPI_Comm communicator,
const int expectedApplications,
const int reportingInterval
)
: mfem::Operator(
operation.Height(),
operation.Width()
),
m_operation(&operation),
m_communicator(communicator),
m_expectedApplications(expectedApplications),
m_reportingInterval(reportingInterval) {
}
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 % m_reportingInterval == 0) {
announce(
m_communicator, "Arnoldi progress: " + std::to_string(m_completedApplications) + "/" +
std::to_string(m_expectedApplications) + " Jacobian applications"
);
}
}
private:
const mfem::Operator *m_operation;
MPI_Comm m_communicator;
int m_expectedApplications;
int m_reportingInterval;
mutable int m_completedApplications{0};
};
[[nodiscard]] mean_field::operators::StellarEquilibriumDependencies make_dependencies() {
return {
.discretization = {.identity = 8101, .revision = 1},
.density = {.identity = 8103, .revision = 1},
.surfaceDeformation = {.identity = 8107, .revision = 1},
.gravityGradient = {.identity = 8111, .revision = 1},
.gravityPotential = {.identity = 8117, .revision = 1},
.enthalpy = {.identity = 8123, .revision = 1},
.bernoulliConstant = {.identity = 8129, .revision = 1},
.rotation = {.identity = 8131, .revision = 1},
.targetMass = {.identity = 8137, .revision = 1}
};
}
[[nodiscard]] mean_field::physics::RigidRotation make_zero_rotation() {
mfem::Vector angularVelocity(3);
mfem::Vector center(3);
angularVelocity = 0.0;
center = 0.0;
return {angularVelocity, center};
}
[[nodiscard]] double global_norm(
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));
}
[[nodiscard]] mfem::Vector make_block_balanced_direction(
const int stateSize,
const std::span<const mean_field::operators::RootBlockDescriptor> valueBlocks,
const MPI_Comm communicator
) {
mfem::Vector direction(stateSize);
direction = 0.0;
for (const mean_field::operators::RootBlockDescriptor &block : valueBlocks) {
mfem::Vector values(direction.GetData() + block.offset, block.size);
for (int index = 0; index < values.Size(); ++index) {
const double ordinal = static_cast<double>(block.canonicalIndex + 1);
values(index) = std::sin(0.6180339887498948 * static_cast<double>(index + 1) + ordinal);
}
const double norm = global_norm(values, communicator);
if (norm > 0.0) {
values /= norm;
}
}
return direction;
}
void require_finite(const double value) {
REQUIRE(std::isfinite(value));
}
[[nodiscard]] std::map<
std::string,
std::string>
common_parameters(
const std::string &measurement,
const int stateSize
) {
return {
{"build_configuration", build_configuration()},
{"equation_of_state", "Polytrope(n=3)"},
{"experiment_schema", "p0_extended_v2"},
{"linearization_state", "projected_lane_emden"},
{"measurement", measurement},
{"mesh_file", test_utils::setup_args().mesh_file},
{"preconditioner", "identity"},
{"preconditioned_product", "J M^-1"},
{"root_dimension", std::to_string(stateSize)}
};
}
} // namespace
TEST_CASE(
"Stellar Equilibrium P0 Identity Preconditioning Baseline",
"[preconditioning][diagnostics][baseline][spectrum]"
) {
using namespace mean_field;
constexpr int arnoldiDimension = 48;
const MPI_Comm world = MPI_COMM_WORLD;
const Clock::time_point experimentStart = Clock::now();
announce(world, "P0 extended baseline: constructing the finite-element discretization");
const Clock::time_point finiteElementSetupStart = Clock::now();
utils::Args args = test_utils::setup_args();
fem::FEM finiteElementModel = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(finiteElementModel.okay());
const MPI_Comm communicator = finiteElementModel.mesh->GetComm();
const double finiteElementSetupSeconds = maximum_rank_seconds(finiteElementSetupStart, communicator);
announce(
communicator, "P0 extended baseline: finite-element setup completed in " +
std::to_string(finiteElementSetupSeconds) + " seconds"
);
constexpr double stellarRadius = utils::RADIUS;
constexpr double targetMass = utils::MASS;
const Clock::time_point calibrationStart = Clock::now();
const seed::DimensionlessLaneEmdenSolution dimensionlessProfile = seed::integrateLaneEmden(3.0, 10.0);
REQUIRE(dimensionlessProfile.firstZeroCoordinate.has_value());
const double surfaceCoordinate = *dimensionlessProfile.firstZeroCoordinate;
const double surfaceDerivative =
dimensionlessProfile.thetaDerivative(dimensionlessProfile.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);
const double calibrationSeconds = maximum_rank_seconds(calibrationStart, communicator);
const Clock::time_point problemConstructionStart = Clock::now();
const auto stellarModel = 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(stellarModel, finiteElementModel);
const double problemConstructionSeconds = maximum_rank_seconds(problemConstructionStart, communicator);
announce(communicator, "P0 extended baseline: projecting the Lane-Emden seed");
const Clock::time_point seedProjectionStart = Clock::now();
const auto projected = seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = 4096}));
const double seedProjectionSeconds = maximum_rank_seconds(seedProjectionStart, communicator);
announce(communicator, "P0 extended baseline: preparing the complete equilibrium operator");
const Clock::time_point operatorPreparationStart = Clock::now();
const operators::PreparedCentralDensityStellarEquilibriumReport preparation =
problem.Prepare(projected.values, make_dependencies(), make_zero_rotation());
REQUIRE(preparation.assembledResidual);
const double operatorPreparationSeconds = maximum_rank_seconds(operatorPreparationStart, communicator);
const mfem::Operator &rawJacobian = problem.GetLinearizationOperator();
mfem::Vector knownDirection =
make_block_balanced_direction(problem.StateSize(), problem.GetManifest().valueBlocks(), communicator);
mfem::Vector rightHandSide(problem.EquationSize());
const Clock::time_point applicationStart = Clock::now();
rawJacobian.Mult(knownDirection, rightHandSide);
const double applicationSeconds = maximum_rank_seconds(applicationStart, communicator);
REQUIRE(rightHandSide.Size() == problem.EquationSize());
require_finite(global_norm(rightHandSide, communicator));
announce(
communicator, "P0 extended baseline: first prepared Jacobian application completed in " +
std::to_string(applicationSeconds) + " seconds"
);
if (std::getenv("MEANFIELD_SINGLE_JACOBIAN_BENCHMARK") != nullptr) {
int rank{0};
MPI_Comm_rank(communicator, &rank);
if (rank == 0) {
std::cout << "Single prepared Jacobian application: " << applicationSeconds << " seconds\n";
}
return;
}
solver::IdentityPreconditioner identity(problem.StateSize());
solver::InstrumentedOperator instrumentedJacobian(rawJacobian);
solver::InstrumentedPreconditioner instrumentedPreconditioner(identity);
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(40);
krylov.SetKDim(20);
krylov.SetPrintLevel(1);
mfem::Vector solution(problem.StateSize());
solution = 0.0;
const operators::PreparedStellarEquilibriumStatistics statisticsBeforeSolve =
problem.GetPreparedOperator().GetPhysicalOperator().GetStatistics();
announce(communicator, "P0 extended baseline: starting the 40-iteration identity-preconditioned FGMRES solve");
const Clock::time_point solveStart = Clock::now();
krylov.Mult(rightHandSide, solution);
const double localSolveSeconds = std::chrono::duration<double>(Clock::now() - solveStart).count();
const operators::PreparedStellarEquilibriumStatistics statisticsAfterSolve =
problem.GetPreparedOperator().GetPhysicalOperator().GetStatistics();
announce(communicator, "P0 extended baseline: independently reconstructing the true residual");
const Clock::time_point directResidualStart = Clock::now();
const solver::LinearSolveMeasurement solveMeasurement = solver::measureLinearSolve(
krylov, rawJacobian, rightHandSide, solution, problem.GetManifest().residualBlocks(),
instrumentedJacobian.GetStatistics(), instrumentedPreconditioner.GetStatistics(),
instrumentedPreconditioner.GetLifecycleStatistics(), monitor, localSolveSeconds, communicator
);
const double directResidualMeasurementSeconds = maximum_rank_seconds(directResidualStart, communicator);
require_finite(solveMeasurement.directResidual.relativeResidual);
require_finite(solveMeasurement.solveSecondsMaximumRank);
std::map<std::string, double> solveMetrics{
{"solver_converged", solveMeasurement.solverConverged ? 1.0 : 0.0},
{"outer_iterations", static_cast<double>(solveMeasurement.outerIterations)},
{"reported_initial_residual_norm", solveMeasurement.solverReportedInitialNorm},
{"reported_final_residual_norm", solveMeasurement.solverReportedFinalNorm},
{"reported_residual_reduction", solveMeasurement.solverReportedResidualReduction},
{"true_residual_norm", solveMeasurement.directResidual.trueResidualNorm},
{"true_relative_residual", solveMeasurement.directResidual.relativeResidual},
{"rhs_norm", solveMeasurement.directResidual.rightHandSideNorm},
{"true_residual_digits_per_jacobian_application",
solveMeasurement.trueResidualDigitsReducedPerJacobianApplication},
{"finite_element_setup_seconds", finiteElementSetupSeconds},
{"lane_emden_calibration_seconds", calibrationSeconds},
{"equilibrium_problem_construction_seconds", problemConstructionSeconds},
{"seed_projection_seconds", seedProjectionSeconds},
{"operator_preparation_seconds", operatorPreparationSeconds},
{"initial_jacobian_application_seconds", applicationSeconds},
{"direct_residual_measurement_seconds", directResidualMeasurementSeconds},
{"solve_seconds_maximum_rank", solveMeasurement.solveSecondsMaximumRank},
{"jacobian_applications", static_cast<double>(solveMeasurement.jacobian.applications)},
{"jacobian_application_seconds", solveMeasurement.jacobian.totalSeconds},
{"jacobian_maximum_application_seconds", solveMeasurement.jacobian.maximumSeconds},
{"inverse_preconditioner_applications",
static_cast<double>(solveMeasurement.inversePreconditioner.applications)},
{"inverse_preconditioner_application_seconds", solveMeasurement.inversePreconditioner.totalSeconds},
{"inverse_preconditioner_maximum_application_seconds", solveMeasurement.inversePreconditioner.maximumSeconds},
{"inverse_preconditioner_setups", static_cast<double>(solveMeasurement.inversePreconditionerLifecycle.setups)},
{"inverse_preconditioner_refreshes",
static_cast<double>(solveMeasurement.inversePreconditionerLifecycle.refreshes)},
{"inverse_preconditioner_setup_seconds", solveMeasurement.inversePreconditionerLifecycle.setupSeconds},
{"inverse_preconditioner_refresh_seconds", solveMeasurement.inversePreconditionerLifecycle.refreshSeconds},
{"prepared_residual_assemblies_during_solve",
static_cast<double>(statisticsAfterSolve.residualAssemblies - statisticsBeforeSolve.residualAssemblies)},
{"prepared_geometry_builds_during_solve",
static_cast<double>(
statisticsAfterSolve.generatedGeometryBuilds - statisticsBeforeSolve.generatedGeometryBuilds
)},
{"prepared_jacobian_applications_during_solve",
static_cast<double>(statisticsAfterSolve.jacobianApplications - statisticsBeforeSolve.jacobianApplications)}
};
for (const solver::ResidualBlockMeasurement &block : solveMeasurement.directResidual.blocks) {
const std::string prefix = "residual_block." + block.stableId;
solveMetrics[prefix + ".descriptor_scale"] = block.descriptorScale;
solveMetrics[prefix + ".rhs_norm"] = block.rightHandSideNorm;
solveMetrics[prefix + ".true_norm"] = block.trueResidualNorm;
solveMetrics[prefix + ".block_relative_residual"] = block.blockRelativeResidual;
solveMetrics[prefix + ".scaled_rhs_norm"] = block.scaledRightHandSideNorm;
solveMetrics[prefix + ".scaled_true_norm"] = block.scaledTrueResidualNorm;
solveMetrics[prefix + ".fraction_global_squared_residual"] = block.fractionOfGlobalSquaredResidualNorm;
solveMetrics[prefix + ".global_relative_contribution"] = block.contributionToGlobalRelativeResidual;
}
experiment::record_experiment_result(
"stellar_preconditioning_p0", "identity_linear_solve", common_parameters("linear_solve", problem.StateSize()),
std::move(solveMetrics)
);
const double reportedInitialDenominator = std::max(solveMeasurement.solverReportedInitialNorm, 1.0e-300);
for (std::size_t sample = 0; sample < solveMeasurement.reportedResidualHistory.size(); ++sample) {
const solver::IterationResidualMeasurement &residual = solveMeasurement.reportedResidualHistory[sample];
experiment::record_experiment_result(
"stellar_preconditioning_p0", "identity_fgmres_history_" + std::to_string(sample),
common_parameters("fgmres_residual_history", problem.StateSize()),
{{"history_sample", static_cast<double>(sample)},
{"iteration", static_cast<double>(residual.iteration)},
{"reported_residual_norm", residual.reportedNorm},
{"reported_relative_residual", residual.reportedNorm / reportedInitialDenominator},
{"final_measurement", residual.final ? 1.0 : 0.0}}
);
}
instrumentedJacobian.ResetStatistics();
instrumentedPreconditioner.ResetStatistics();
solver::FixedRightPreconditionedOperator rightPreconditionedProduct(
instrumentedJacobian, instrumentedPreconditioner
);
ArnoldiProgressOperator progressOperator(rightPreconditionedProduct, communicator, arnoldiDimension, 4);
announce(
communicator,
"P0 extended baseline: starting the " + std::to_string(arnoldiDimension) + "-vector Arnoldi measurement"
);
const solver::ArnoldiSpectralMeasurement spectrum = solver::measureArnoldiSpectrum(
progressOperator, knownDirection, communicator,
{.krylovDimension = arnoldiDimension,
.breakdownRelativeTolerance = 1.0e-13,
.ritzConvergenceRelativeTolerance = 1.0e-7,
.reorthogonalize = true}
);
require_finite(spectrum.projectedLargestSingularValue);
require_finite(spectrum.centroidRealPart);
require_finite(spectrum.rmsClusterRadius);
experiment::record_experiment_result(
"stellar_preconditioning_p0", "identity_arnoldi_summary",
common_parameters("arnoldi_summary", problem.StateSize()),
{{"requested_krylov_dimension", static_cast<double>(spectrum.requestedDimension)},
{"achieved_krylov_dimension", static_cast<double>(spectrum.achievedDimension)},
{"invariant_subspace_found", spectrum.invariantSubspaceFound ? 1.0 : 0.0},
{"operator_applications", static_cast<double>(spectrum.operatorApplications)},
{"arnoldi_operator_application_seconds", spectrum.operatorApplicationSecondsMaximumRank},
{"arnoldi_operator_maximum_application_seconds", spectrum.operatorMaximumApplicationSecondsMaximumRank},
{"arnoldi_measurement_seconds", spectrum.measurementSecondsMaximumRank},
{"arnoldi_nonapplication_seconds", spectrum.nonApplicationSecondsMaximumRank},
{"experiment_elapsed_through_arnoldi_seconds", maximum_rank_seconds(experimentStart, communicator)},
{"converged_ritz_values", static_cast<double>(spectrum.convergedRitzValueCount)},
{"negative_real_part_ritz_values", static_cast<double>(spectrum.negativeRealPartCount)},
{"projected_largest_singular_value", spectrum.projectedLargestSingularValue},
{"projected_smallest_singular_value", spectrum.projectedSmallestSingularValue},
{"projected_condition_proxy", spectrum.projectedConditionProxy},
{"ritz_centroid_real", spectrum.centroidRealPart},
{"ritz_centroid_imaginary", spectrum.centroidImaginaryPart},
{"ritz_rms_distance_from_one", spectrum.rmsDistanceFromOne},
{"ritz_rms_cluster_radius", spectrum.rmsClusterRadius},
{"ritz_minimum_magnitude", spectrum.minimumMagnitude},
{"ritz_maximum_magnitude", spectrum.maximumMagnitude},
{"ritz_minimum_real_part", spectrum.minimumRealPart},
{"ritz_maximum_real_part", spectrum.maximumRealPart},
{"ritz_maximum_absolute_imaginary_part", spectrum.maximumAbsoluteImaginaryPart},
{"ritz_conjugate_pair_defect", spectrum.conjugatePairDefect},
{"projected_departure_from_normality", spectrum.projectedDepartureFromNormality},
{"projected_field_of_values_minimum_real_part", spectrum.projectedFieldOfValuesMinimumRealPart},
{"projected_field_of_values_maximum_real_part", spectrum.projectedFieldOfValuesMaximumRealPart},
{"measured_jacobian_applications", static_cast<double>(instrumentedJacobian.GetStatistics().applications)},
{"measured_jacobian_application_seconds", instrumentedJacobian.GetStatistics().totalSeconds},
{"measured_jacobian_maximum_application_seconds", instrumentedJacobian.GetStatistics().maximumSeconds},
{"measured_inverse_preconditioner_applications",
static_cast<double>(instrumentedPreconditioner.GetStatistics().applications)},
{"measured_inverse_preconditioner_application_seconds",
instrumentedPreconditioner.GetStatistics().totalSeconds}}
);
std::vector<solver::RitzValueMeasurement> orderedRitzValues = spectrum.ritzValues;
std::ranges::sort(orderedRitzValues, [](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 < orderedRitzValues.size(); ++index) {
const solver::RitzValueMeasurement &ritz = orderedRitzValues[index];
experiment::record_experiment_result(
"stellar_preconditioning_p0", "identity_ritz_" + std::to_string(index),
common_parameters("ritz_value", problem.StateSize()),
{{"ritz_index", static_cast<double>(index)},
{"ritz_real", ritz.realPart},
{"ritz_imaginary", ritz.imaginaryPart},
{"ritz_magnitude", ritz.magnitude},
{"ritz_distance_from_one", ritz.distanceFromOne},
{"ritz_residual_estimate", ritz.residualEstimate},
{"ritz_relative_residual_estimate", ritz.relativeResidualEstimate},
{"ritz_converged", ritz.converged ? 1.0 : 0.0}}
);
}
int rank{0};
MPI_Comm_rank(communicator, &rank);
if (rank == 0) {
std::cout << "P0 identity baseline: " << solveMeasurement.outerIterations << " FGMRES iterations, "
<< spectrum.achievedDimension << " Arnoldi vectors, true relative residual "
<< solveMeasurement.directResidual.relativeResidual << '\n';
}
}