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

@@ -1,10 +1,10 @@
module;
#include <concepts>
#include <mpi.h>
#include <cstdint>
#include <expected>
#include <memory>
#include <mpi.h>
#include <span>
#include <stdexcept>
#include <string>
@@ -15,6 +15,8 @@ module;
export module mean_field:normalization.stellar_equilibrium;
import :deformation.surface_metric;
export import :normalization.operators;
export import :operators.stellar_equilibrium_compiler;
export import :operators.stellar_equilibrium_problem;
@@ -23,15 +25,18 @@ export import :utils.domain;
namespace mean_field::normalization::detail {
using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema;
[[nodiscard]] inline bool CanAssembleExactPartialDiagonal(mfem::ParFiniteElementSpace &space, bool hdiv) {
[[nodiscard]] inline bool CanAssembleExactPartialDiagonal(
mfem::ParFiniteElementSpace &space,
bool hdiv
) {
// For hanging nodes, diag(P^T A P) contains cross terms. MFEM's PA
// diagonal is approximate there, so retain assembled exact diagonals.
int eligible = space.GetMesh()->Conforming() && !space.IsVariableOrder() && space.GetVDim() == 1;
for (int e = 0; eligible && e < space.GetNE(); ++e) {
const auto *fe = space.GetFE(e);
eligible = fe->GetGeomType() == mfem::Geometry::CUBE &&
(hdiv ? dynamic_cast<const mfem::RT_HexahedronElement *>(fe) != nullptr :
dynamic_cast<const mfem::NodalTensorFiniteElement *>(fe) != nullptr);
eligible = fe->GetGeomType() == mfem::Geometry::CUBE &&
(hdiv ? dynamic_cast<const mfem::RT_HexahedronElement *>(fe) != nullptr
: dynamic_cast<const mfem::NodalTensorFiniteElement *>(fe) != nullptr);
}
int allEligible = 0;
if (MPI_Allreduce(&eligible, &allEligible, 1, MPI_INT, MPI_MIN, space.GetComm()) != MPI_SUCCESS)
@@ -40,7 +45,9 @@ namespace mean_field::normalization::detail {
}
[[nodiscard]] inline mfem::Vector AssembleMassDiagonal(
mfem::ParBilinearForm &mass, mfem::ParFiniteElementSpace &space, bool partial
mfem::ParBilinearForm &mass,
mfem::ParFiniteElementSpace &space,
bool partial
) {
if (partial) {
mass.SetAssemblyLevel(mfem::AssemblyLevel::PARTIAL);
@@ -52,7 +59,8 @@ namespace mean_field::normalization::detail {
mass.Assemble();
mass.Finalize();
std::unique_ptr<mfem::HypreParMatrix> matrix(mass.ParallelAssemble());
if (!matrix) throw std::runtime_error("Reference Riesz mass assembly failed.");
if (!matrix)
throw std::runtime_error("Reference Riesz mass assembly failed.");
mfem::Vector diagonal;
matrix->GetDiag(diagonal);
return diagonal;
@@ -61,7 +69,7 @@ namespace mean_field::normalization::detail {
[[nodiscard]] inline mfem::Vector AssembleScalarMassDiagonal(
mfem::ParFiniteElementSpace &space,
mfem::Array<int> *domainMarker = nullptr,
bool reference = false
bool reference = false
) {
mfem::ParBilinearForm mass(&space);
if (domainMarker == nullptr) {
@@ -72,20 +80,25 @@ namespace mean_field::normalization::detail {
return AssembleMassDiagonal(mass, space, !reference && CanAssembleExactPartialDiagonal(space, false));
}
[[nodiscard]] inline mfem::Vector AssembleHDivMassDiagonal(mfem::ParFiniteElementSpace &space, bool reference = false) {
[[nodiscard]] inline mfem::Vector AssembleHDivMassDiagonal(
mfem::ParFiniteElementSpace &space,
bool reference = false
) {
mfem::ParBilinearForm mass(&space);
mass.AddDomainIntegrator(new mfem::VectorFEMassIntegrator());
return AssembleMassDiagonal(mass, space, !reference && CanAssembleExactPartialDiagonal(space, true));
}
[[nodiscard]] inline bool CanAssembleDirectBoundaryDiagonal(
mfem::ParFiniteElementSpace &space, int attribute
mfem::ParFiniteElementSpace &space,
int attribute
) {
int eligible = !space.Nonconforming() && !space.IsVariableOrder() && space.GetVDim() == 1 &&
dynamic_cast<const mfem::H1_FECollection *>(space.FEColl()) != nullptr;
dynamic_cast<const mfem::H1_FECollection *>(space.FEColl()) != nullptr;
mfem::Array<int> dofs;
for (int b = 0; eligible && b < space.GetNBE(); ++b) {
if (space.GetBdrAttribute(b) != attribute) continue;
if (space.GetBdrAttribute(b) != attribute)
continue;
// A general DOF transformation would require transforming the
// full element matrix before extracting its diagonal.
eligible = space.GetBdrElementVDofs(b, dofs) == nullptr;
@@ -97,7 +110,8 @@ namespace mean_field::normalization::detail {
}
[[nodiscard]] inline mfem::Vector AssembleDirectBoundaryDiagonal(
mfem::ParFiniteElementSpace &space, int attribute
mfem::ParFiniteElementSpace &space,
int attribute
) {
mfem::Vector localDiagonal(space.GetVSize());
localDiagonal = 0.0;
@@ -106,13 +120,14 @@ namespace mean_field::normalization::detail {
// GetNBE includes tagged internal interfaces. MFEM's boundary PA face
// restriction uses FaceType::Boundary and omits the star/vacuum interface.
for (int b = 0; b < space.GetNBE(); ++b) {
if (space.GetBdrAttribute(b) != attribute) continue;
const auto &element = *space.GetBE(b);
if (space.GetBdrAttribute(b) != attribute)
continue;
const auto &element = *space.GetBE(b);
auto &transformation = *space.GetBdrElementTransformation(b);
space.GetBdrElementVDofs(b, dofs);
shape.SetSize(element.GetDof());
elementDiagonal.SetSize(element.GetDof());
elementDiagonal = 0.0;
elementDiagonal = 0.0;
// Identical rule and physical shapes to MassIntegrator's default
// element assembly; only the diagonal of w * shape * shape^T is formed.
const auto &rule = mfem::MassIntegrator::GetRule(element, element, transformation);
@@ -183,14 +198,25 @@ export namespace mean_field::normalization {
// full assembled true-DOF diagonal; production uses exact PA for supported
// volume spaces and direct marked-boundary quadrature for supported surfaces.
[[nodiscard]] inline mfem::Vector AssembleReferenceScalarMassDiagonal(
mfem::ParFiniteElementSpace &space, mfem::Array<int> *domainMarker = nullptr, bool reference = false
) { return detail::AssembleScalarMassDiagonal(space, domainMarker, reference); }
mfem::ParFiniteElementSpace &space,
mfem::Array<int> *domainMarker = nullptr,
bool reference = false
) {
return detail::AssembleScalarMassDiagonal(space, domainMarker, reference);
}
[[nodiscard]] inline mfem::Vector AssembleReferenceHDivMassDiagonal(
mfem::ParFiniteElementSpace &space, bool reference = false
) { return detail::AssembleHDivMassDiagonal(space, reference); }
mfem::ParFiniteElementSpace &space,
bool reference = false
) {
return detail::AssembleHDivMassDiagonal(space, reference);
}
[[nodiscard]] inline mfem::Vector AssembleReferenceSurfaceMassDiagonal(
const fem::FEM &finiteElements, const field::ScalarBoundaryDofMap &surfaceMap, bool reference = false
) { return detail::AssembleSurfaceMassDiagonal(finiteElements, surfaceMap, reference); }
const fem::FEM &finiteElements,
const field::ScalarBoundaryDofMap &surfaceMap,
bool reference = false
) {
return detail::AssembleSurfaceMassDiagonal(finiteElements, surfaceMap, reference);
}
/*
* Runtime preparation paired with the compile-time normalization plan.
@@ -544,9 +570,20 @@ export namespace mean_field::normalization {
detail::AssembleScalarMassDiagonal(*finiteElements.gravityPotentialFes),
gravityContext.GetGravityPotentialMap(), "gravity-potential"
);
const field::ScalarBoundaryDofMap surfaceMap =
field::make_stellar_surface_scalar_dof_map<detail::DomainSchema>(*finiteElements.surfaceDeformationFes);
const mfem::Vector surfaceDiagonal = detail::AssembleSurfaceMassDiagonal(finiteElements, surfaceMap);
const mfem::Vector surfaceDiagonal = [&]() {
const auto &domain = physical.GetDomainDeformation();
if (domain.hasNodalRadialParameters()) {
const field::ScalarBoundaryDofMap surfaceMap =
field::make_stellar_surface_scalar_dof_map<detail::DomainSchema>(
*finiteElements.surfaceDeformationFes
);
return detail::AssembleSurfaceMassDiagonal(finiteElements, surfaceMap);
}
// Modal coefficients are not surface nodal values. Pulling back
// the reference trace metric gives one primal Gram entry per
// actual parameter; the builder supplies its dual residual scale.
return deformation::assembleSurfaceParameterMassDiagonal(finiteElements, domain);
}();
if (surfaceDiagonal.Size() != physical.GetDomainDeformation().parameterCount()) {
throw std::logic_error("The reference surface Gram diagonal does not match the root surface block.");
}