Files
MeanField/experiments/spherical_harmonic_surface_diagnostics.hpp

126 lines
6.7 KiB
C++

#pragma once
// Include after import mean_field. These observations do not participate in
// acceptance: an equilibrium in the selected shape space can retain force
// components conjugate to the boundary motions excluded by that choice.
#include <cmath>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <stdexcept>
#include <string>
namespace experiment::plateau_diagnostics {
template <class State>
void RecordHarmonicSurface(
State &state,
const mean_field::fem::FEM &fem,
mean_field::deformation::SphericalHarmonicSurfaceOptions options,
const std::filesystem::path &output,
const std::string &label
) {
using namespace mean_field;
using Schema = utils::domain::CoreEnvelopeVacuumDomainSchema;
const auto map = field::make_stellar_surface_scalar_dof_map<Schema>(*fem.surfaceDeformationFes);
const auto surface = deformation::compileSurfaceDeformationPrescription(
deformation::SphericalHarmonicSurface{options},
deformation::SurfaceDeformationCompilationContext{*fem.surfaceDeformationFes, map}
);
mfem::Vector center(3);
center = 0.;
const auto extensionContext = deformation::makeRadialDeformationExtensionCompilationContext<Schema>(
*fem.surfaceDeformationFes, *fem.displacementFes, *fem.logicalReferenceMesh
);
const auto nodal = deformation::composePreparedDomainDeformation(
deformation::compileSurfaceDeformationPrescription(
deformation::NodalRadialSurface{center},
deformation::SurfaceDeformationCompilationContext{*fem.surfaceDeformationFes, map}
),
deformation::compileInteriorDeformationExtension(
deformation::PowerLawRadialInteriorExtension{}, extensionContext
),
deformation::compileVacuumDeformationExtension(
deformation::FixedInfinityRadialVacuumExtension{}, extensionContext
),
*fem.surfaceDeformationFes, *fem.displacementFes, *fem.logicalReferenceMesh
);
const auto &physical = state.Problem().GetPhysicalOperator();
const auto &basis = surface.nodalBasis();
const auto &coefficients = physical.GetSurfaceDeformationParameters();
if (coefficients.Size() != basis.Width())
throw std::logic_error("Harmonic observer received incompatible coordinates.");
mfem::Vector radial(basis.Height()), force(basis.Height()), volume(nodal.volumeDisplacementSize());
basis.Mult(coefficients, radial);
nodal.buildVolumeDisplacement(radial, volume);
volume -= physical.GetGeneratedVolumeDisplacement();
nodal.applyJacobianTranspose(radial, physical.GetFullMechanicalResidual(), force);
mfem::Vector retained(basis.Width()), native(basis.Width());
basis.MultTranspose(force, retained);
physical.GetDomainDeformation().applyJacobianTranspose(
coefficients, physical.GetFullMechanicalResidual(), native
);
native -= retained;
for (const auto *vector : {&force, &retained, &native, &volume, &radial})
for (int index = 0; index < vector->Size(); ++index)
if (!std::isfinite((*vector)(index)))
throw std::runtime_error("Harmonic observer received nonfinite geometry or force.");
if (volume.Norml2() > 1e-11 * (1. + physical.GetGeneratedVolumeDisplacement().Norml2()) ||
native.Norml2() > 1e-11 * (1. + retained.Norml2()))
throw std::runtime_error("Harmonic observer failed its nodal geometry or dual-work equivalence check.");
// The Euclidean complement is an explicitly identified diagnostic norm,
// not a replacement for the solver's dimensioned Riesz residual norm.
mfem::DenseMatrix orthogonal(basis);
mfem::Vector excluded(force), column(basis.Height());
for (int mode = 0; mode < basis.Width(); ++mode) {
for (int row = 0; row < basis.Height(); ++row)
column(row) = orthogonal(row, mode);
for (int pass = 0; pass < 2; ++pass)
for (int previous = 0; previous < mode; ++previous) {
long double projection = 0.;
for (int row = 0; row < basis.Height(); ++row)
projection += column(row) * orthogonal(row, previous);
for (int row = 0; row < basis.Height(); ++row)
column(row) -= projection * orthogonal(row, previous);
}
const double norm = column.Norml2();
if (!(norm > 0.) || !std::isfinite(norm))
throw std::runtime_error("Unresolved harmonic observer basis.");
column /= norm;
excluded.Add(-(column * force), column);
for (int row = 0; row < basis.Height(); ++row)
orthogonal(row, mode) = column(row);
}
const auto file = [&](const std::string &name) {
std::ofstream stream(output / name);
stream.exceptions(std::ios::badbit | std::ios::failbit);
stream << std::setprecision(17);
return stream;
};
auto modes = file(label + "_surface_coefficients.csv");
modes << "index,degree,order,radial_length_coefficient\n";
for (int mode = 0; mode < basis.Width(); ++mode)
modes << mode << ',' << surface.modes()[mode].degree << ',' << surface.modes()[mode].order << ','
<< coefficients(mode) << '\n';
auto forces = file(label + "_surface_nodal_force.csv");
forces << "node,radial_displacement,force,excluded_force\n";
for (int node = 0; node < basis.Height(); ++node)
forces << node << ',' << radial(node) << ',' << force(node) << ',' << excluded(node) << '\n';
auto metrics = file(label + "_surface_audit.csv");
metrics << "metric,value\nparameter_count," << basis.Width() << "\nnodal_count," << basis.Height()
<< "\nfull_nodal_force_euclidean_norm," << force.Norml2() << "\nexcluded_nodal_force_euclidean_norm,"
<< excluded.Norml2() << "\nmodal_force_euclidean_norm," << retained.Norml2()
<< "\nvolume_equivalence_error," << volume.Norml2() << "\nforce_pullback_equivalence_error,"
<< native.Norml2() << '\n';
if (label == "initial") {
auto savedBasis = file("surface_basis.csv");
for (int node = 0; node < basis.Height(); ++node) {
for (int mode = 0; mode < basis.Width(); ++mode)
savedBasis << (mode ? "," : "") << basis(node, mode);
savedBasis << '\n';
}
}
}
} // namespace experiment::plateau_diagnostics