From 52474587ddee8bd6684668d52935b9729be946d5 Mon Sep 17 00:00:00 2001 From: Emily Boudreaux Date: Thu, 17 Sep 2026 16:45:55 -0400 Subject: [PATCH] Guard coarse exactness diagnostics and report complete setup work --- .../plateau_preconditioner_benchmark.hpp | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/experiments/plateau_preconditioner_benchmark.hpp b/experiments/plateau_preconditioner_benchmark.hpp index b8125fa..33b1ceb 100644 --- a/experiments/plateau_preconditioner_benchmark.hpp +++ b/experiments/plateau_preconditioner_benchmark.hpp @@ -253,8 +253,8 @@ void BenchmarkPreconditioners(State &state, const std::filesystem::path &output, reference << "context_inverse_application_seconds," << detail::Seconds(start) << "\ncontext_inverse_correction_norm," << referenceCorrection.Norml2() << '\n'; reference.flush(); - if (!detail::Finite(referenceCorrection)) - throw std::runtime_error("The context reference inverse action is nonfinite."); + if (!detail::Finite(referenceCorrection) || !std::isfinite(referenceCorrection.Norml2())) + throw std::runtime_error("The context reference inverse action or its norm is nonfinite."); detail::Save(output / "context_reference_normalized_correction.txt", referenceCorrection); verify("context_reference"); @@ -336,6 +336,8 @@ void BenchmarkPreconditioners(State &state, const std::filesystem::path &output, metric("setup_jacobian_applications", stats.setupOperatorApplications); metric("setup_inverse_applications", stats.setupInverseApplications); metric("setup_seconds", stats.lastSetupSeconds); + metric("setup_jacobian_seconds", stats.lastSetupOperatorSeconds); + metric("setup_inverse_seconds", stats.lastSetupInverseSeconds); metric("persistent_vector_bytes", stats.persistentVectorBytes); metric("applications", stats.applications); metric("inverse_applications", stats.inverseApplications); @@ -346,9 +348,19 @@ void BenchmarkPreconditioners(State &state, const std::filesystem::path &output, sectionStart = detail::Clock::now(); const auto modes = MakeCoupledShapeModes(state.Problem(), physical); const double basisSeconds = detail::Seconds(sectionStart); + recordWork("after_basis"); + // Direct basis calls bypass the structure-factorization counters. + // The material inverse also counts its usual internal couplings. + for (const char *metric : {"basis_explicit_surface_to_material_applications", + "basis_material_inverse_applications", + "basis_material_to_gravity_applications", + "basis_gravity_inverse_applications"}) + coarseReport << role << ',' << variant << ",constructed," << metric << ',' << modes.size() << '\n'; + const auto coarseStart = detail::Clock::now(); coarse = std::make_unique(*state.normalizedOperator, normalized, modes, state.Problem().GetCommunicator()); - const double completeCoarseSeconds = detail::Seconds(sectionStart); + const double completeCoarseSeconds = basisSeconds + detail::Seconds(coarseStart); + recordWork("after_coarse_setup"); coarseReport << role << ',' << variant << ",constructed,physical_inverse_construction_seconds," << constructionSeconds << '\n'; constructionSeconds += completeCoarseSeconds; @@ -366,10 +378,20 @@ void BenchmarkPreconditioners(State &state, const std::filesystem::path &output, state.normalizedOperator->Mult(value, image); coarse->Mult(image, recovered); recovered -= value; - maximumRelativeError = std::max(maximumRelativeError, - detail::Relative(recovered.Norml2(), value.Norml2())); + const double valueNorm = value.Norml2(), imageNorm = image.Norml2(), errorNorm = recovered.Norml2(); + if (!detail::Finite(value) || !detail::Finite(image) || !detail::Finite(recovered) || + !std::isfinite(valueNorm) || valueNorm <= 0.0 || !std::isfinite(imageNorm) || + !std::isfinite(errorNorm)) + throw std::runtime_error("The actual FEM coarse identity produced a nonfinite or null reference."); + const double relativeError = errorNorm / valueNorm; + // std::max can discard NaN, so validate each observation first. + if (!std::isfinite(relativeError)) + throw std::runtime_error("The actual FEM coarse relative identity error is nonfinite."); + maximumRelativeError = std::max(maximumRelativeError, relativeError); } - coarseReport << role << ',' << variant << ",validation,maximum_exact_coarse_relative_error," + coarseReport << role << ',' << variant << ",validation,exact_coarse_modes_checked," + << coarse->GetSourceBasis().size() << '\n' + << role << ',' << variant << ",validation,maximum_exact_coarse_relative_error," << maximumRelativeError << '\n'; coarseReport.flush(); if (!std::isfinite(maximumRelativeError) || maximumRelativeError > 1.e-8) @@ -380,11 +402,15 @@ void BenchmarkPreconditioners(State &state, const std::filesystem::path &output, sectionStart = detail::Clock::now(); applicationInverse->Mult(rhs, inverseAction); const double applicationSeconds = detail::Seconds(sectionStart); + if (!detail::Finite(inverseAction) || !std::isfinite(inverseAction.Norml2())) + throw std::runtime_error("The diagnostic inverse action or its norm is nonfinite."); detail::Save(output / (role + "_inverse_rhs_action.txt"), inverseAction); sectionStart = detail::Clock::now(); state.normalizedOperator->Mult(inverseAction, inverseJacobianAction); const double actionSeconds = detail::Seconds(sectionStart); const double actionNorm = inverseJacobianAction.Norml2(); + if (!detail::Finite(inverseJacobianAction) || !std::isfinite(actionNorm)) + throw std::runtime_error("The diagnostic inverse Jacobian image or its norm is nonfinite."); inverseJacobianAction -= rhs; mfem::Vector referenceDifference(inverseAction); referenceDifference -= referenceCorrection; @@ -518,12 +544,14 @@ void BenchmarkPreconditioners(State &state, const std::filesystem::path &output, << "The fresh independent inverse must match the existing context inverse on that RHS.\n" << "construction_seconds includes physical inverse and any selected coarse/basis construction.\n" << "Coarse exact-subspace validation is diagnostic work outside setup/solve totals.\n" + << "An inactive coarse fallback has exact_coarse_modes_checked=0; its zero maximum error is vacuous.\n" + << "Stellar-level inverse/cross counters exclude direct basis calls, recorded separately; gravity leaf counters include them.\n" << "unchanged_state_refresh_seconds measures an immediate Refresh; it is not a changed-state refresh benchmark.\n" << "Each candidate receives one diagnostic inverse/Jv application before the timed cold Krylov solve.\n" << "Application timers/counts in the summary include only Krylov work; the independent true check is separate.\n" << "setup_plus_solve_seconds includes construction, normalization binding, Krylov setup, and Krylov solve.\n" << "setup_solve_and_true_check_seconds adds the separately measured original-J true-residual gate.\n" - << "measured_candidate_seconds also includes diagnostic refresh and the inverse/Jv comparison.\n" + << "measured_candidate_seconds also includes diagnostic refresh, coarse exactness checks, and inverse/Jv comparison.\n" << "Common FEM/context construction and frozen-state preparation occur before this helper.\n" << "The helper performs no trial preparation or nonlinear update; WithState restores its accepted seed afterward.\n" << "Block norms are coefficient norms in the stated normalized/physical coordinates, not spatial error norms.\n"