Add native spherical harmonic surface coordinates to coupled equilibrium

This commit is contained in:
2026-09-19 08:51:42 -04:00
parent 2171b8b76a
commit 7b0eb8d92d
23 changed files with 3131 additions and 57 deletions

View File

@@ -32,6 +32,7 @@ import mean_field;
#include "plateau_polynomial_certificate.hpp"
#include "shape_field_weak_diagnostics.hpp"
#include "reduced_surface_diagnostics.hpp"
#include "spherical_harmonic_surface_diagnostics.hpp"
using namespace mean_field;
namespace {
@@ -44,7 +45,8 @@ struct Options {
bool preconditionerProbe = false, nonlinearStepProbe = false;
bool weakShapeProbe = false, weakShapeFinal = false;
bool reducedSurfaceProbe = false, reducedSurfaceAlgebraCheck = false;
int surfaceDegree = 4;
int surfaceDegree = 4, surfaceMaximumOrder = -1;
std::string surfacePrescription = "nodal";
bool coupledTrialProbe = false, coupledSeedControl = false;
bool dampedCoupledProbe = false;
std::string coupledSearch = "near";
@@ -122,6 +124,10 @@ Options Parse(int argc, char **argv) {
o.reducedSurfaceProbe = true;
else if (a == "--reduced-surface-algebra-check")
o.reducedSurfaceAlgebraCheck = true;
else if (a == "--surface-prescription")
o.surfacePrescription = v();
else if (a == "--surface-maximum-order")
o.surfaceMaximumOrder = std::stoi(v());
else if (a == "--surface-degree")
o.surfaceDegree = std::stoi(v());
else if (a == "--weak-shape-probe")
@@ -186,6 +192,23 @@ Options Parse(int argc, char **argv) {
}
if (o.output.empty() || o.n < 1 || o.n >= 5 || o.maxNewton < 0 || o.maxLinear < 1 || o.restart < 1)
throw std::invalid_argument("Invalid options");
if (o.surfacePrescription != "nodal" && o.surfacePrescription != "harmonics")
throw std::invalid_argument("Surface prescriptions are nodal or harmonics");
if (o.surfacePrescription == "harmonics") {
deformation::SphericalHarmonicSurfaceOptions surfaceOptions{.maximumDegree = o.surfaceDegree};
if (o.surfaceMaximumOrder != -1) surfaceOptions.maximumAzimuthalOrder = o.surfaceMaximumOrder;
surfaceOptions.validate();
// Older frozen probes interpret snapshots as nodal radial values. A
// size match is insufficient to establish modal snapshot provenance.
if (o.reducedSurfaceProbe || o.reducedSurfaceAlgebraCheck || o.weakShapeProbe || o.weakShapeFinal ||
o.probes || o.pressureProbe || o.shapeCurvature || o.surfaceTrace || o.strategyProbe ||
o.positivityProbe || o.positivityCertificate || o.preconditionerProbe || o.nonlinearStepProbe ||
o.coupledTrialProbe || o.dampedCoupledProbe || !o.probeState.empty() || !o.probeDirection.empty() ||
!o.replayCandidate.empty() || o.strategy != "baseline")
throw std::invalid_argument("Native harmonic diagnostics currently support live baseline Newton trajectories only");
} else if (o.surfaceMaximumOrder != -1) {
throw std::invalid_argument("--surface-maximum-order requires --surface-prescription harmonics");
}
if (o.reducedSurfaceProbe || o.reducedSurfaceAlgebraCheck) {
if ((o.reducedSurfaceProbe && o.reducedSurfaceAlgebraCheck) || o.maxNewton != 0 ||
o.strategy != "baseline" || o.probes || o.pressureProbe || o.shapeCurvature ||
@@ -323,6 +346,15 @@ void SaveVector(const std::filesystem::path &p, const mfem::Vector &v) {
v.Print(s, 1);
}
mfem::Vector LoadVector(const std::filesystem::path &path, int expectedSize) {
// Legacy nodal probes may share a total vector size with another shape
// space. Honor known coordinate provenance before reading its numbers.
for (const auto &directory : {path.parent_path(), path.parent_path().parent_path()}) {
std::ifstream metadata(directory / "metadata.txt");
std::string line;
while (std::getline(metadata, line))
if (line == "surface_prescription=harmonics")
throw std::invalid_argument("Legacy nodal probes cannot load a harmonic snapshot: " + path.string());
}
std::ifstream input(path);
int count = -1;
input >> count;
@@ -560,6 +592,8 @@ int RunContext(Context &context, const Options &o, const Scales &scales) {
<< "\ngravity_iterations=" << o.gravityIterations << "\ngravity_restart=" << o.gravityRestart
<< "\npreconditioner_probe=" << o.preconditionerProbe
<< "\nnonlinear_step_probe=" << o.nonlinearStepProbe
<< "\nsurface_prescription=" << o.surfacePrescription
<< "\nsurface_maximum_order=" << o.surfaceMaximumOrder
<< "\nreduced_surface_probe=" << o.reducedSurfaceProbe << "\nsurface_degree=" << o.surfaceDegree
<< "\nweak_shape_probe=" << o.weakShapeProbe << "\nweak_shape_final=" << o.weakShapeFinal
<< "\ndamped_coupled_probe=" << o.dampedCoupledProbe
@@ -762,6 +796,11 @@ int RunContext(Context &context, const Options &o, const Scales &scales) {
experiment::polytrope_validation::PhysicalState physical(s, f);
auto out = File(o.output / (label + "_physical.csv"));
out << "quadrature,metric,value\n";
if (o.surfacePrescription == "harmonics") {
deformation::SphericalHarmonicSurfaceOptions surfaceOptions{.maximumDegree = o.surfaceDegree};
if (o.surfaceMaximumOrder != -1) surfaceOptions.maximumAzimuthalOrder = o.surfaceMaximumOrder;
experiment::plateau_diagnostics::RecordHarmonicSurface(s, f, surfaceOptions, o.output, label);
}
for (int q : {14, 18}) {
auto metrics = experiment::plateau_diagnostics::MeasurePhysical(physical, o.n, scales.K, q);
metrics["normalized_bordered_residual"] = physical.normalizedBorderedResidualNorm;
@@ -1065,9 +1104,11 @@ int RunContext(Context &context, const Options &o, const Scales &scales) {
});
return converged ? 0 : 3;
}
template <class Model> int Setup(Model model, fem::FEM f, const Options &o, const Scales &scales) {
template <class Model, class Surface>
int SetupWithSurface(Model model, fem::FEM f, Surface surface, const Options &o, const Scales &scales) {
auto d = equilibrium::makeStellarDiscretization(
std::move(f), normalization::PhysicalRieszDiagonal{dimensions::LengthValue{1.}, utils::G});
std::move(f), normalization::PhysicalRieszDiagonal{dimensions::LengthValue{1.}, utils::G},
std::move(surface));
const auto withRecipe = [&](auto recipe) {
using Recipe = decltype(recipe);
using Problem = equilibrium::StellarEquilibriumProblem<Model, decltype(d)>;
@@ -1095,6 +1136,18 @@ template <class Model> int Setup(Model model, fem::FEM f, const Options &o, cons
{.correctionSweeps = o.schurCorrectionSweeps, .relaxation = o.schurRelaxation}});
throw std::logic_error("An unsupported Newton preconditioner bypassed option validation.");
}
template <class Model> int Setup(Model model, fem::FEM f, const Options &o, const Scales &scales) {
if (o.surfacePrescription == "harmonics") {
deformation::SphericalHarmonicSurfaceOptions options{.maximumDegree = o.surfaceDegree};
if (o.surfaceMaximumOrder != -1) options.maximumAzimuthalOrder = o.surfaceMaximumOrder;
return SetupWithSurface(std::move(model), std::move(f),
deformation::SphericalHarmonicSurface{options}, o, scales);
}
mfem::Vector center(3);
center = 0.0;
return SetupWithSurface(std::move(model), std::move(f),
deformation::NodalRadialSurface{std::move(center)}, o, scales);
}
int Run(const Options &o) {
const auto scales = Derive(o.n);
std::cout << "n=" << o.n << " K=" << scales.K << " rho_c=" << scales.rho << " J=" << o.angularMomentum << std::endl;