#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include import mean_field; #include "geometry_quality_diagnostics.hpp" namespace { using namespace mean_field; using Clock = std::chrono::steady_clock; struct Options { std::string mesh = "sandbox.smesh"; std::filesystem::path output = "geometry_quality_results"; std::vector tolerances{0.03, 0.003}; int maximumLinearIterations = 200; int advance = 0; bool finiteDifferences = true; bool blockActions = true; bool geometryOnly = false; bool saveVectors = false; bool warm = false; std::filesystem::path replayVectors; bool diagonalOnly=false; }; Options Parse(int argc, char **argv) { Options o; for (int i = 1; i < argc; ++i) { const std::string arg = argv[i]; auto value = [&]() -> std::string { if (++i >= argc) throw std::invalid_argument("Missing value after " + arg); return argv[i]; }; if (arg == "--mesh") o.mesh = value(); else if (arg == "--output") o.output = value(); else if (arg == "--tolerances") { o.tolerances.clear(); std::istringstream stream(value()); for (std::string token; std::getline(stream, token, ',');) { const double tolerance = std::stod(token); if (!(tolerance > 0.0 && tolerance < 1.0)) throw std::invalid_argument("Invalid tolerance"); o.tolerances.push_back(tolerance); } if (o.tolerances.empty()) throw std::invalid_argument("Empty tolerance list"); } else if (arg == "--max-linear-iterations") o.maximumLinearIterations = std::stoi(value()); else if (arg == "--advance") o.advance = std::stoi(value()); else if (arg == "--no-fd") o.finiteDifferences = false; else if (arg == "--no-block-actions") o.blockActions = false; else if (arg == "--geometry-only") o.geometryOnly = true; else if (arg == "--save-vectors") o.saveVectors = true; else if (arg == "--warm") o.warm = true; else if (arg == "--replay-vectors") o.replayVectors=value(); else if (arg == "--diagonal-only") o.diagonalOnly=true; else if (arg == "--help") { std::cout << "geometry_quality_experiment [--mesh FILE] [--output NEW_DIRECTORY]\n" " [--tolerances 0.03,0.003] [--max-linear-iterations 200] [--advance N]\n" " [--warm] [--geometry-only] [--no-fd] [--no-block-actions] [--save-vectors]\n" " [--replay-vectors CASE_vectors.csv] (verifies saved accepted state, no linear solve)\n" " [--diagonal-only] (seed replay only; skips per-element scans and residual checks)\n" "Single MPI rank only. Defaults inspect a frozen seed; --advance uses production Newton.\n"; std::exit(0); } else throw std::invalid_argument("Unknown option: " + arg); } if (o.advance < 0 || o.maximumLinearIterations < 1) throw std::invalid_argument("Invalid iteration count"); if (!o.replayVectors.empty()) o.tolerances.resize(1); if (o.diagonalOnly) { if (o.replayVectors.empty() || o.advance!=0 || o.geometryOnly) throw std::invalid_argument("--diagonal-only requires seed correction replay"); o.finiteDifferences=false; o.blockActions=false; } return o; } std::ofstream File(const std::filesystem::path &path) { std::ofstream stream(path); stream.exceptions(std::ios::failbit | std::ios::badbit); stream << std::setprecision(17); return stream; } double Norm(const mfem::Vector &v, int offset, int size) { long double sum = 0; for (int i = offset; i < offset + size; ++i) sum += static_cast(v(i)) * v(i); return std::sqrt(sum); } double MaxAbs(const mfem::Vector &v, int offset, int size) { double result = 0; for (int i = offset; i < offset + size; ++i) result = std::max(result, std::abs(v(i))); return result; } template void Inspect(State &s, const fem::FEM &fem, const Options &options) { auto &problem = s.Problem(); const auto &deformation = problem.GetPhysicalOperator().GetDomainDeformation(); // Recompile only the public surface-coordinate descriptor to inspect its // radii/directions; the actual extension always remains the context's. mfem::Vector center(3); center=0.0; const deformation::SurfaceDeformationCompilationContext surfaceContext{ *fem.surfaceDeformationFes, field::make_stellar_surface_scalar_dof_map(*fem.surfaceDeformationFes)}; const auto surface=deformation::compileSurfaceDeformationPrescription(deformation::NodalRadialSurface{center},surfaceContext); const auto values = problem.GetManifest().valueBlocks(); const auto rows = problem.GetManifest().residualBlocks(); const auto surfaceIterator = std::find_if(values.begin(), values.end(), [](const auto &b) { return b.stableId == "surface_deformation"; }); if (surfaceIterator == values.end()) throw std::logic_error("Experiment requires a surface-deformation block"); const auto surfaceBlock = *surfaceIterator; const mfem::Vector acceptedVolume(problem.GetPhysicalOperator().GetGeneratedVolumeDisplacement()); const mfem::Vector physicalResidual(s.normalizedOperator->GetPhysicalResidual()); const mfem::Vector warmCorrection(s.normalizedCorrection); auto residuals = File(options.output / "blocks.csv"); residuals << "case,kind,block,size,physical_l2,normalized_l2,physical_linf,normalized_linf,linear_residual_over_block_F\n"; auto record = [&](const std::string &label, const std::string &kind, auto blocks, const mfem::Vector &physical, const mfem::Vector &normalized, bool relative) { for (const auto &b : blocks) { const double denominator = relative ? Norm(s.acceptedNormalizedResidual, b.offset, b.size) : 0.0; residuals << label << ',' << kind << ',' << b.stableId << ',' << b.size << ',' << Norm(physical, b.offset, b.size) << ',' << Norm(normalized, b.offset, b.size) << ',' << MaxAbs(physical, b.offset, b.size) << ',' << MaxAbs(normalized, b.offset, b.size) << ',' << (relative && denominator > 0 ? Norm(normalized, b.offset, b.size) / denominator : std::numeric_limits::quiet_NaN()) << '\n'; } residuals.flush(); }; record("accepted", "residual", rows, physicalResidual, s.acceptedNormalizedResidual, false); record("accepted", "state", values, s.AcceptedPhysicalState(), s.acceptedNormalizedState, false); auto metadata = File(options.output / "metadata.txt"); metadata << "mesh=" << options.mesh << "\nmesh_bytes=" << std::filesystem::file_size(options.mesh) << "\ncompiled=" << __DATE__ << ' ' << __TIME__ << "\ncompiler=" << __VERSION__ << "\npolynomial_increment=" << MEAN_FIELD_UNIFORM_POLYNOMIAL_ORDER_INCREMENT << "\nmpi_ranks=1\nmodel=n1_K2G_R2_over_pi_M1_R1_J0_fixed_central_density" << "\nnormalization=production_frozen_physical_Riesz_diagonal" << "\npreconditioner=production_default\nFGMRES_restart=40\nadvanced_steps=" << options.advance << "\nmax_linear_iterations=" << options.maximumLinearIterations << "\nwarm=" << options.warm << "\nstate_size=" << problem.StateSize() << "\nelement_count=" << fem.mesh->GetNE() << "\naccepted_residual=" << s.acceptedNormalizedResidual.Norml2() << "\ncoefficient_norms_are_unweighted_single_rank=true" << "\nsurface_mean_and_rms_are_nodal_not_area_weighted=true\ntolerances="; for (double t : options.tolerances) metadata << t << ','; metadata << '\n'; metadata << "replay_vectors=" << options.replayVectors.string() << '\n'; metadata << "diagonal_only=" << options.diagonalOnly << '\n'; metadata.flush(); auto vertexGeometry = [&](const std::string &label,const mfem::Vector &direction) { // Deliberately independent sampling of the known sandbox core corners. // Uses the unmodified production estimator with a vertex-only rule. std::vector rules; for (int element : {0,9,18,27,36,45,54,63}) { if (elementGetNE() && fem.mesh->GetAttribute(element)==1 && fem.mesh->GetElementBaseGeometry(element)==mfem::Geometry::CUBE) rules.push_back({element,mfem::Geometries.GetVertices(mfem::Geometry::CUBE)}); } if (!rules.empty()) experiment::InspectGeometry(problem.GetDiscretization().domainMapper(),*fem.displacementFes, *fem.compactificationCoordinate,acceptedVolume,direction,rules,options.output,label+"_vertices"); }; auto geometry = [&](const std::string &label, const mfem::Vector &physicalDirection) { mfem::Vector volumeDirection(acceptedVolume.Size()); problem.BuildVolumeDisplacementDirection(physicalDirection, volumeDirection); std::cout << "Geometry: " << label << std::endl; if (options.advance==0 && (label=="newton_0" || label=="uniform_contraction")) { int corner=0; double minimumSum=std::numeric_limits::infinity(); for (int i=0;i coreRules; for (const auto &rule : s.geometryPreflightRules) { if (fem.mesh->GetAttribute(rule.element)==1) coreRules.push_back(rule); } if (!options.diagonalOnly) for (bool radial : {false,true}) { mfem::VectorFunctionCoefficient coefficient(3,[radial](const mfem::Vector &x,mfem::Vector &u) { u=x; u *= radial ? -x.Norml2()/utils::RADIUS : -1.0; }); mfem::ParGridFunction projected(fem.displacementFes.get()); projected.ProjectCoefficient(coefficient); mfem::Vector direction; projected.GetTrueDofs(direction); experiment::InspectGeometry(problem.GetDiscretization().domainMapper(), *fem.displacementFes, *fem.compactificationCoordinate, acceptedVolume, direction, coreRules, options.output, radial ? "core_projected_physical_radial_contraction" : "core_projected_affine_contraction"); } if (options.geometryOnly) return; auto solves = File(options.output / "solves.csv"); solves << "case,tolerance,status,iterations,relative_true_residual,wall_seconds,safe_step,boundary_step,limiting_element,correction_difference_from_baseline,surface_difference_from_baseline,action_difference_over_F\n"; auto surfaceFile = File(options.output / "surface.csv"); surfaceFile << "case,parameter,x,y,z,radius,accepted_fraction,correction_fraction\n"; auto surfaceSummary = File(options.output / "surface_summary.csv"); surfaceSummary << "case,mean_fraction,rms_fraction,rms_nonmean_fraction,min_fraction,max_fraction\n"; auto columns = File(options.output / "block_actions.csv"); columns << "case,column,row,normalized_action_l2,normalized_dot_with_F\n"; auto fd = File(options.output / "finite_differences.csv"); fd << "case,epsilon,row,relative_error,absolute_error,action_norm\n"; auto extensionChecks = File(options.output / "extension_checks.csv"); extensionChecks << "case,epsilon,relative_volume_direction_error,trial_residual_norm\n"; mfem::Vector baseline, baselineAction; for (std::size_t caseIndex = 0; caseIndex < options.tolerances.size(); ++caseIndex) { const std::string label = "newton_" + std::to_string(caseIndex); const double tolerance = options.tolerances[caseIndex]; s.normalizedCorrection = options.warm ? warmCorrection : mfem::Vector(problem.StateSize()); if (!options.warm) s.normalizedCorrection = 0.0; s.linearRightHandSide = s.acceptedNormalizedResidual; s.linearRightHandSide *= -1; std::cout << "Solve: " << label << " tolerance=" << tolerance << std::endl; const auto start = Clock::now(); solver::LinearSolveReport solve; if (options.replayVectors.empty()) { solve=s.linearBackend->Solve(s.linearRightHandSide, s.normalizedCorrection, {.relativeTolerance=tolerance, .absoluteTolerance=0.0, .maximumIterations=options.maximumLinearIterations}); } else { std::ifstream saved(options.replayVectors); if (!saved) throw std::runtime_error("Cannot open replay vectors"); std::string line; std::getline(saved,line); if (line!="index,physical_state,normalized_state,physical_correction,normalized_correction") throw std::runtime_error("Unrecognized replay vector header"); int index=0; while (std::getline(saved,line)) { std::replace(line.begin(),line.end(),',',' '); std::istringstream row(line); int savedIndex; double physicalState, normalizedState, physicalCorrection, normalizedCorrection; if (!(row>>savedIndex>>physicalState>>normalizedState>>physicalCorrection>>normalizedCorrection) || savedIndex!=index || index>=problem.StateSize()) throw std::runtime_error("Invalid replay vector row"); if (std::abs(physicalState-s.AcceptedPhysicalState()(index))>1e-12*(1+std::abs(physicalState)) || std::abs(normalizedState-s.acceptedNormalizedState(index))>1e-12*(1+std::abs(normalizedState))) throw std::runtime_error("Replay state differs from this frozen context"); if (!std::isfinite(normalizedCorrection)) throw std::runtime_error("Nonfinite replay direction"); s.normalizedCorrection(index++)=normalizedCorrection; } if (index!=problem.StateSize()) throw std::runtime_error("Incomplete replay vector file"); mfem::Vector check(problem.EquationSize()); s.normalizedOperator->Mult(s.normalizedCorrection,check); check+=s.acceptedNormalizedResidual; solve.relativeTrueResidualNorm=check.Norml2()/s.acceptedNormalizedResidual.Norml2(); solve.status=solve.relativeTrueResidualNorm<=tolerance ? solver::LinearSolveStatus::converged : solver::LinearSolveStatus::maximum_iterations; solve.iterations=0; std::cout << "Replayed saved correction; true residual independently checked" << std::endl; } const double elapsed = std::chrono::duration(Clock::now()-start).count(); std::cout << "Solved: iterations=" << solve.iterations << " true_relative=" << solve.relativeTrueResidualNorm << " wall=" << elapsed << std::endl; s.normalizedOperator->DenormalizeState(s.normalizedCorrection, s.physicalCorrection); const auto safe = s.EstimateLargestSafeStepSize(1.0, 0.9); geometry(label, s.physicalCorrection); if (caseIndex==0 && options.advance==0 && !options.replayVectors.empty()) { mfem::Vector zeroSurface(surfaceBlock.size), surfaceDirection(surfaceBlock.size); zeroSurface=0.0; for (int i=0;i(power)); int corner=0; double smallest=std::numeric_limits::infinity(); for (int i=0;iMult(s.normalizedCorrection, action); linearResidual = action; linearResidual += s.acceptedNormalizedResidual; s.normalizedOperator->DenormalizeResidual(linearResidual, physicalLinear); record(label, "linear_residual", rows, physicalLinear, linearResidual, true); record(label, "correction", values, s.physicalCorrection, s.normalizedCorrection, false); if (caseIndex == 0) { baseline = s.normalizedCorrection; baselineAction=action; } mfem::Vector difference(s.normalizedCorrection); difference -= baseline; mfem::Vector actionDifference(action); actionDifference-=baselineAction; solves << label << ',' << tolerance << ',' << static_cast(solve.status) << ',' << solve.iterations << ',' << solve.relativeTrueResidualNorm << ',' << elapsed << ',' << safe.stepSize << ',' << safe.boundaryStepSize << ',' << safe.limitingElement << ',' << difference.Norml2()/std::max(baseline.Norml2(),1e-300) << ',' << Norm(difference,surfaceBlock.offset,surfaceBlock.size)/std::max(Norm(baseline,surfaceBlock.offset,surfaceBlock.size),1e-300) << ',' << actionDifference.Norml2()/std::max(s.acceptedNormalizedResidual.Norml2(),1e-300) << '\n'; solves.flush(); double sum=0, squared=0, minimum=std::numeric_limits::infinity(), maximum=-minimum; for (int i=0; iMult(direction,blockAction); for (const auto &row : rows) { double dot=0; for (int i=row.offset;i0)) throw std::runtime_error("No admissible finite-difference step"); std::cout << "Finite difference epsilon=" << epsilon << std::endl; s.trialNormalizedState=s.acceptedNormalizedState; s.trialNormalizedState.Add(epsilon,s.normalizedCorrection); const auto preparation=s.PrepareTrial(); if (!preparation) throw std::runtime_error("Finite-difference trial preparation rejected"); mfem::Vector volumeError(problem.GetPhysicalOperator().GetGeneratedVolumeDisplacement()); volumeError-=acceptedVolume; volumeError/=epsilon; volumeError-=expectedVolumeDirection; extensionChecks << label << ',' << epsilon << ',' << volumeError.Norml2()/std::max(expectedVolumeDirection.Norml2(),1e-300) << ',' << s.trialNormalizedResidual.Norml2() << '\n'; extensionChecks.flush(); mfem::Vector approximation(s.trialNormalizedResidual); approximation-=s.acceptedNormalizedResidual; approximation/=epsilon; approximation-=action; for (const auto &row:rows) { const double error=Norm(approximation,row.offset,row.size), magnitude=Norm(action,row.offset,row.size); fd << label << ',' << epsilon << ',' << row.stableId << ',' << (magnitude>0 ? error/magnitude : std::numeric_limits::quiet_NaN()) << ',' << error << ',' << magnitude << '\n'; } fd.flush(); } s.RestoreAccepted(); } } } } // namespace int main(int argc, char **argv) { mfem::Mpi::Init(argc,argv); try { const auto options=Parse(argc,argv); int ranks=0; MPI_Comm_size(MPI_COMM_WORLD,&ranks); if (ranks!=1) throw std::invalid_argument("This diagnostic executable currently requires exactly one MPI rank"); if (std::filesystem::exists(options.output)) throw std::invalid_argument("Output directory already exists; use a new --output path"); std::filesystem::create_directories(options.output); mfem::Device device("cpu"); utils::Args args; args.mesh_file=options.mesh; args.p.rtol=1e-12; args.p.atol=1e-12; const auto start=Clock::now(); auto fem=fem::setup_fem(args.mesh_file,args,0); if (!fem.okay()) throw std::runtime_error("Finite-element setup failed"); constexpr double radius=utils::RADIUS, mass=utils::MASS, G=utils::G; auto model=model::StellarModel(eos::Polytrope({.n=1.0,.K=2*G*radius*radius/std::numbers::pi}), surface::Isobaric({.Psurf=dimensions::PressureValue{0}}), integral::FixedTotalMass({.Mtotal=dimensions::MassValue{mass}}), integral::FixedAngularMomentum({.Jtotal=dimensions::AngularMomentumValue{0},.axis={0,0,1},.center={0,0,0}}), constraint::FixedCentralDensity({.RhoC=dimensions::DensityValue{std::numbers::pi*mass/(4*radius*radius*radius)}})); auto discretization=equilibrium::makeStellarDiscretization(std::move(fem), normalization::PhysicalRieszDiagonal{dimensions::LengthValue{radius},G}); std::cout << "Constructing production context" << std::endl; auto context=solver::makeContext(std::move(model),std::move(discretization), preconditioning::makePreconditioner(),solver::linear::FGMRES({.restartLength=40,.printLevel=-1})); std::cout << "Setup seconds=" << std::chrono::duration(Clock::now()-start).count() << std::endl; if (options.advance>0) { auto trajectory=File(options.output/"trajectory.csv"); trajectory << "iteration,residual,step,limiting_element,boundary\n"; auto observer=solver::nonlinear::makeObserver([](const solver::nonlinear::BeforeIteration &) {}, [&](const solver::nonlinear::AfterIteration &event) { trajectory << event.iteration << ',' << event.residualNorm << ',' << event.acceptedStepLength << ',' << (event.geometryPreflight ? event.geometryPreflight->limitingElement : -1) << ',' << (event.geometryPreflight ? event.geometryPreflight->boundaryStepSize : 0) << '\n'; trajectory.flush(); std::cout << "Advance iteration=" << event.iteration << " residual=" << event.residualNorm << " step=" << event.acceptedStepLength << std::endl; }); auto method=solver::nonlinear::Newton(solver::nonlinear::NewtonOptions{ .relativeTolerance=1e-8,.absoluteTolerance=0,.maximumIterations=options.advance, .linearSolve={.relativeTolerance=0.03,.absoluteTolerance=0,.maximumIterations=200},.backtracking={}}); auto solve=solver::make(context,method,observer); auto report=solve.evaluate(); std::cout << "Production accepted steps=" << report.completedNonlinearIterations() << std::endl; } solver::detail::StellarEquilibriumContextDiagnostics::WithState(context,[&](auto &state, auto &fem) { Inspect(state,fem,options); }); if (!context.isReady() || context.hasActiveSolver()) throw std::logic_error("Diagnostic access did not restore the context"); std::cout << "Experiment complete: " << options.output << " total_seconds=" << std::chrono::duration(Clock::now()-start).count() << std::endl; return 0; } catch (const std::exception &error) { std::cerr << "geometry_quality_experiment: " << error.what() << '\n'; return 1; } }