#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 #include #include #include #include #include namespace experiment::plateau_diagnostics { template 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(*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( *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