#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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(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 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(index + 1); values(index) = std::sin(0.6180339887498948 * ordinal + phase + static_cast(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 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 &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(spectrum.requestedDimension)}, {"achieved_dimension", static_cast(spectrum.achievedDimension)}, {"invariant_subspace_found", spectrum.invariantSubspaceFound ? 1.0 : 0.0}, {"operator_applications", static_cast(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(jacobianStatistics.applications)}, {"measured_jacobian_application_seconds", jacobianStatistics.totalSeconds}, {"measured_jacobian_maximum_application_seconds", jacobianStatistics.maximumSeconds}, {"measured_preconditioner_applications", static_cast(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(spectrum.negativeRealPartCount)}, {"converged_ritz_value_count", static_cast(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 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(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(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::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 metrics{ {"maximum_iterations", static_cast(maximumIterations)}, {"restart_dimension", static_cast(restartDimension)}, {"solver_converged", solve.solverConverged ? 1.0 : 0.0}, {"outer_iterations", static_cast(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(iteration1e2)}, {"reported_iteration_to_1e-4", static_cast(iteration1e4)}, {"reported_iteration_to_1e-6", static_cast(iteration1e6)}, {"reported_iteration_to_1e-8", static_cast(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(solve.jacobian.applications)}, {"jacobian_application_seconds", solve.jacobian.totalSeconds}, {"jacobian_maximum_application_seconds", solve.jacobian.maximumSeconds}, {"preconditioner_applications", static_cast(solve.inversePreconditioner.applications)}, {"preconditioner_application_seconds", solve.inversePreconditioner.totalSeconds}, {"preconditioner_maximum_application_seconds", solve.inversePreconditioner.maximumSeconds}, {"preconditioner_setups", static_cast(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(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(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(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::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(index)}, {"iteration", static_cast(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 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; 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(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 metrics{ {"maximum_iterations", static_cast(maximumIterations)}, {"restart_dimension", static_cast(restartDimension)}, {"solver_converged", solve.solverConverged ? 1.0 : 0.0}, {"outer_iterations", static_cast(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::min())}, {"correction_norm", correctionNorm}, {"solve_seconds_maximum_rank", solve.solveSecondsMaximumRank}, {"jacobian_applications", static_cast(solve.jacobian.applications)}, {"jacobian_application_seconds", solve.jacobian.totalSeconds}, {"preconditioner_applications", static_cast(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(index)}, {"iteration", static_cast(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; 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 * dimensionlessMass); const double polytropicConstant = std::numbers::pi_v * utils::G * std::pow(massScale, 2.0 / 3.0); const double radialScale = stellarRadius / surfaceCoordinate; const double centralDensity = std::pow(polytropicConstant / (std::numbers::pi_v * 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 * dimensionlessMass); const double polytropicConstant = std::numbers::pi_v * utils::G * std::pow(massScale, 2.0 / 3.0); const double radialScale = stellarRadius / surfaceCoordinate; const double centralDensity = std::pow(polytropicConstant / (std::numbers::pi_v * 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 * dimensionlessMass); const double polytropicConstant = std::numbers::pi_v * utils::G * std::pow(massScale, 2.0 / 3.0); const double radialScale = stellarRadius / surfaceCoordinate; const double centralDensity = std::pow(polytropicConstant / (std::numbers::pi_v * 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 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 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::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 metrics{ {"alpha", alpha}, {"dependency_revision", static_cast(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::min()); metrics["actual_to_predicted_norm_ratio"] = actualNorm / std::max(predictedNorm, std::numeric_limits::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::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) ); } } }