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

@@ -0,0 +1,381 @@
module;
#include <algorithm>
#include <cmath>
#include <limits>
#include <numbers>
#include <stdexcept>
#include <utility>
#include <vector>
#include <mfem.hpp>
#include <mpi.h>
module mean_field;
import :deformation.spherical_harmonic_surface;
namespace mean_field::deformation {
namespace {
[[nodiscard]] SurfaceDeformationDescriptor harmonicDescriptor() noexcept {
return {
.name = "SphericalHarmonicSurface",
.spatialDimension = 3,
.motionKind = SurfaceMotionKind::Radial,
.linearOnReferenceGeometry = true,
.requiresStarShapedReferenceSurface = true,
.hasExactDerivativeTranspose = true,
.hasExactPullbackDerivative = true,
// A spectral cutoff alone is not a proof that either physical
// gauge is removed; retain the conservative nodal descriptor.
.translationTreatment = GeometricGaugeTreatment::Retained,
.orientationTreatment = GeometricGaugeTreatment::Retained
};
}
[[nodiscard]] long long countModes(const SphericalHarmonicSurfaceOptions &options) noexcept {
const long long degree = options.maximumDegree;
const long long order = options.maximumAzimuthalOrder.value_or(options.maximumDegree);
return (order + 1) * (order + 1) + (degree - order) * (2 * order + 1);
}
[[nodiscard]] mfem::Vector zeroCenter() {
mfem::Vector center(3);
center = 0.;
return center;
}
void requireFinite(
const mfem::Vector &values,
const char *message
) {
for (int i = 0; i < values.Size(); ++i)
if (!std::isfinite(values(i)))
throw std::invalid_argument(message);
}
[[nodiscard]] double checkedDouble(long double value) {
const double result = static_cast<double>(value);
if (!std::isfinite(result))
throw std::invalid_argument("Spherical harmonic surface action produced a nonfinite value.");
return result;
}
// Recurrence on normalized associated Legendre functions avoids the
// factorial growth of an unnormalized polynomial followed by scaling.
[[nodiscard]] std::vector<double> evaluateModes(
const double x,
const double y,
const double z,
const int maximumDegree,
const int maximumOrder,
const std::vector<SphericalHarmonicMode> &modes
) {
const std::size_t stride = static_cast<std::size_t>(maximumOrder) + 1;
std::vector<double> legendre((static_cast<std::size_t>(maximumDegree) + 1) * stride, 0.);
const auto p = [&](int l, int m) -> double & { return legendre[static_cast<std::size_t>(l) * stride + m]; };
const double azimuth = std::atan2(y, x), cylindricalRadius = std::hypot(x, y);
p(0, 0) = 1. / std::sqrt(4. * std::numbers::pi);
for (int m = 0; m <= maximumOrder; ++m) {
if (m > 0)
p(m, m) = -std::sqrt((2. * m + 1.) / (2. * m)) * cylindricalRadius * p(m - 1, m - 1);
if (m < maximumDegree)
p(m + 1, m) = std::sqrt(2. * m + 3.) * z * p(m, m);
for (int l = m + 2; l <= maximumDegree; ++l) {
const double ll = l, mm = m;
const double a = std::sqrt((4. * ll * ll - 1.) / (ll * ll - mm * mm));
const double b = std::sqrt(
((2. * ll + 1.) * ((ll - 1.) * (ll - 1.) - mm * mm)) / ((2. * ll - 3.) * (ll * ll - mm * mm))
);
p(l, m) = a * z * p(l - 1, m) - b * p(l - 2, m);
}
}
std::vector<double> values;
values.reserve(modes.size());
for (const auto mode : modes) {
double value = p(mode.degree, std::abs(mode.order));
if (mode.order < 0)
value *= std::sqrt(2.) * std::sin(-mode.order * azimuth);
if (mode.order > 0)
value *= std::sqrt(2.) * std::cos(mode.order * azimuth);
if (!std::isfinite(value))
throw std::invalid_argument("Nonfinite spherical harmonic reference basis.");
values.push_back(value);
}
return values;
}
[[nodiscard]] long double columnDot(
const mfem::DenseMatrix &matrix,
int first,
int second
) {
long double result = 0.;
for (int row = 0; row < matrix.Height(); ++row)
result += static_cast<long double>(matrix(row, first)) * matrix(row, second);
return result;
}
void requireIndependentModes(const mfem::DenseMatrix &basis) {
mfem::DenseMatrix orthogonal(basis);
// This detects unresolved/aliased nodal modes without changing
// their physical normalization or silently dropping coordinates.
constexpr double minimumRelativePivot = 1e-12;
for (int column = 0; column < basis.Width(); ++column) {
const double original = std::sqrt(checkedDouble(columnDot(basis, column, column)));
for (int pass = 0; pass < 2; ++pass)
for (int previous = 0; previous < column; ++previous) {
const double projection = checkedDouble(columnDot(orthogonal, previous, column));
for (int row = 0; row < basis.Height(); ++row)
orthogonal(row, column) -= projection * orthogonal(row, previous);
}
const double pivot = std::sqrt(checkedDouble(columnDot(orthogonal, column, column)));
if (!(original > 0.) || !std::isfinite(pivot) || !(pivot > minimumRelativePivot * original))
throw std::invalid_argument(
"Selected spherical harmonics are numerically dependent on the surface nodes; "
"reduce their degree/order or refine the surface discretization."
);
for (int row = 0; row < basis.Height(); ++row)
orthogonal(row, column) /= pivot;
}
}
} // namespace
void SphericalHarmonicSurfaceOptions::validate() const {
if (maximumDegree < 0 ||
(maximumAzimuthalOrder && (*maximumAzimuthalOrder < 0 || *maximumAzimuthalOrder > maximumDegree)))
throw std::invalid_argument(
"Spherical harmonic degree must be nonnegative and the azimuthal cutoff must lie in [0,L]."
);
if (countModes(*this) > std::numeric_limits<int>::max())
throw std::invalid_argument("The spherical harmonic mode count exceeds supported vector indexing.");
}
SphericalHarmonicSurface::SphericalHarmonicSurface(SphericalHarmonicSurfaceOptions options)
: SphericalHarmonicSurface(
zeroCenter(),
std::move(options)
) {
}
SphericalHarmonicSurface::SphericalHarmonicSurface(
mfem::Vector referenceCenter,
SphericalHarmonicSurfaceOptions options
)
: m_referenceCenter(std::move(referenceCenter)),
m_options(std::move(options)) {
validate();
}
const mfem::Vector &SphericalHarmonicSurface::referenceCenter() const noexcept {
return m_referenceCenter;
}
const SphericalHarmonicSurfaceOptions &SphericalHarmonicSurface::options() const noexcept {
return m_options;
}
int SphericalHarmonicSurface::modeCount() const noexcept {
return static_cast<int>(countModes(m_options));
}
SurfaceDeformationDescriptor SphericalHarmonicSurface::descriptor() const noexcept {
return harmonicDescriptor();
}
void SphericalHarmonicSurface::validate() const {
m_options.validate();
if (m_referenceCenter.Size() != 3)
throw std::invalid_argument("SphericalHarmonicSurface requires a three-dimensional reference center.");
requireFinite(m_referenceCenter, "Spherical harmonic reference-center coordinates must be finite.");
}
PreparedSphericalHarmonicSurface::PreparedSphericalHarmonicSurface(
PreparedNodalRadialSurface radialSurface,
std::vector<SphericalHarmonicMode> modes,
mfem::DenseMatrix nodalBasis
)
: m_radialSurface(std::move(radialSurface)),
m_modes(std::move(modes)),
m_nodalBasis(std::move(nodalBasis)) {
}
SurfaceDeformationDescriptor PreparedSphericalHarmonicSurface::descriptor() const noexcept {
return harmonicDescriptor();
}
int PreparedSphericalHarmonicSurface::parameterCount() const noexcept {
return static_cast<int>(m_modes.size());
}
long long PreparedSphericalHarmonicSurface::globalParameterCount() const noexcept {
return parameterCount();
}
long long PreparedSphericalHarmonicSurface::globalParameterOffset() const noexcept {
return 0;
}
int PreparedSphericalHarmonicSurface::spatialDimension() const noexcept {
return 3;
}
int PreparedSphericalHarmonicSurface::surfaceNodeCount() const noexcept {
return m_radialSurface.parameterCount();
}
int PreparedSphericalHarmonicSurface::surfaceDisplacementSize() const noexcept {
return m_radialSurface.surfaceDisplacementSize();
}
long long PreparedSphericalHarmonicSurface::globalSurfaceDisplacementSize() const noexcept {
return m_radialSurface.globalSurfaceDisplacementSize();
}
long long PreparedSphericalHarmonicSurface::globalSurfaceDisplacementOffset() const noexcept {
return m_radialSurface.globalSurfaceDisplacementOffset();
}
int PreparedSphericalHarmonicSurface::surfaceDisplacementDof(
int node,
int component
) const {
return m_radialSurface.surfaceDisplacementDof(node, component);
}
double PreparedSphericalHarmonicSurface::radialDirection(
int node,
int component
) const {
return m_radialSurface.radialDirection(node, component);
}
double PreparedSphericalHarmonicSurface::referenceRadius(int node) const {
return m_radialSurface.referenceRadius(node);
}
const mfem::Vector &PreparedSphericalHarmonicSurface::referenceCenter() const noexcept {
return m_radialSurface.referenceCenter();
}
const field::ScalarBoundaryDofMap &PreparedSphericalHarmonicSurface::surfaceDofMap() const noexcept {
return m_radialSurface.surfaceDofMap();
}
std::span<const SphericalHarmonicMode> PreparedSphericalHarmonicSurface::modes() const noexcept {
return m_modes;
}
const mfem::DenseMatrix &PreparedSphericalHarmonicSurface::nodalBasis() const noexcept {
return m_nodalBasis;
}
void PreparedSphericalHarmonicSurface::requireParameters(const mfem::Vector &parameters) const {
if (parameters.Size() != parameterCount())
throw std::invalid_argument("Spherical harmonic parameter vector has an incompatible size.");
requireFinite(parameters, "Spherical harmonic parameters and directions must be finite.");
}
void PreparedSphericalHarmonicSurface::requireSurfaceSize(const mfem::Vector &surfaceDisplacement) const {
if (surfaceDisplacement.Size() != surfaceDisplacementSize())
throw std::invalid_argument("Spherical harmonic nodal surface vector has an incompatible size.");
}
void PreparedSphericalHarmonicSurface::applyEmbedding(
const mfem::Vector &parameters,
mfem::Vector &surfaceDisplacement
) const {
for (int node = 0; node < surfaceNodeCount(); ++node) {
long double amplitude = 0.;
for (int mode = 0; mode < parameterCount(); ++mode)
amplitude += static_cast<long double>(m_nodalBasis(node, mode)) * parameters(mode);
const double radialAmplitude = checkedDouble(amplitude);
for (int component = 0; component < 3; ++component)
surfaceDisplacement(surfaceDisplacementDof(node, component)) =
checkedDouble(static_cast<long double>(radialAmplitude) * radialDirection(node, component));
}
}
void PreparedSphericalHarmonicSurface::buildSurfaceDisplacement(
const mfem::Vector &parameters,
mfem::Vector &surfaceDisplacement
) const {
requireParameters(parameters);
requireSurfaceSize(surfaceDisplacement);
applyEmbedding(parameters, surfaceDisplacement);
}
void PreparedSphericalHarmonicSurface::applyJacobian(
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
mfem::Vector &surfaceDisplacementDirection
) const {
requireParameters(parameters);
requireParameters(parameterDirection);
requireSurfaceSize(surfaceDisplacementDirection);
applyEmbedding(parameterDirection, surfaceDisplacementDirection);
}
void PreparedSphericalHarmonicSurface::applyJacobianTranspose(
const mfem::Vector &parameters,
const mfem::Vector &surfaceDisplacementDual,
mfem::Vector &parameterDual
) const {
requireParameters(parameters);
requireSurfaceSize(surfaceDisplacementDual);
requireFinite(surfaceDisplacementDual, "Spherical harmonic surface dual must be finite.");
if (parameterDual.Size() != parameterCount())
throw std::invalid_argument("Spherical harmonic parameter dual has an incompatible size.");
for (int mode = 0; mode < parameterCount(); ++mode) {
long double work = 0.;
for (int node = 0; node < surfaceNodeCount(); ++node) {
long double radialWork = 0.;
for (int component = 0; component < 3; ++component)
radialWork += static_cast<long double>(radialDirection(node, component)) *
surfaceDisplacementDual(surfaceDisplacementDof(node, component));
work += static_cast<long double>(m_nodalBasis(node, mode)) * radialWork;
}
parameterDual(mode) = checkedDouble(work);
}
}
void PreparedSphericalHarmonicSurface::applyPullbackDerivative(
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
const mfem::Vector &surfaceDisplacementDual,
mfem::Vector &parameterDualAction
) const {
requireParameters(parameters);
requireParameters(parameterDirection);
requireSurfaceSize(surfaceDisplacementDual);
requireFinite(surfaceDisplacementDual, "Spherical harmonic surface dual must be finite.");
if (parameterDualAction.Size() != parameterCount())
throw std::invalid_argument("Spherical harmonic pullback action has an incompatible size.");
// The basis is compiled on the reference surface and never reweighted
// as the star moves, so the derivative of its transpose is exactly zero.
parameterDualAction = 0.;
}
PreparedSphericalHarmonicSurface compileSurfaceDeformationPrescription(
const SphericalHarmonicSurface &prescription,
const SurfaceDeformationCompilationContext &context
) {
int ranks = 0;
if (MPI_Comm_size(context.scalarFiniteElementSpace().GetComm(), &ranks) != MPI_SUCCESS)
throw std::runtime_error("Could not inspect the spherical harmonic surface communicator.");
// A replicated K-vector would be double-counted by distributed Newton
// inner products. Reject it until modal ownership is implemented.
if (ranks != 1)
throw std::invalid_argument(
"Spherical harmonic surface compilation currently supports one MPI rank; distributed modal ownership "
"is not implemented."
);
prescription.validate();
auto radial =
compileSurfaceDeformationPrescription(NodalRadialSurface{prescription.referenceCenter()}, context);
if (radial.spatialDimension() != 3)
throw std::invalid_argument("Spherical harmonics require a three-dimensional reference mesh.");
if (prescription.modeCount() > radial.parameterCount())
throw std::invalid_argument("Selected spherical harmonic mode count exceeds the number of surface nodes.");
const int degree = prescription.options().maximumDegree;
const int cutoff = prescription.options().maximumAzimuthalOrder.value_or(degree);
std::vector<SphericalHarmonicMode> modes;
modes.reserve(prescription.modeCount());
for (int l = 0; l <= degree; ++l)
for (int m = -std::min(l, cutoff); m <= std::min(l, cutoff); ++m)
modes.push_back({l, m});
mfem::DenseMatrix basis(radial.parameterCount(), prescription.modeCount());
for (int node = 0; node < radial.parameterCount(); ++node) {
const auto values = evaluateModes(
radial.radialDirection(node, 0), radial.radialDirection(node, 1), radial.radialDirection(node, 2),
degree, cutoff, modes
);
for (int mode = 0; mode < prescription.modeCount(); ++mode)
basis(node, mode) = values[mode];
}
requireIndependentModes(basis);
return PreparedSphericalHarmonicSurface(std::move(radial), std::move(modes), std::move(basis));
}
} // namespace mean_field::deformation

View File

@@ -0,0 +1,89 @@
module;
#include <cmath>
#include <memory>
#include <stdexcept>
#include <mfem.hpp>
#include <mpi.h>
module mean_field;
import :deformation.surface_metric;
import :field.mfem;
import :utils.domain;
namespace mean_field::deformation {
// Parameter coordinates need their own reference metric: a modal
// coefficient is not the value at a boundary node. This assembles the
// diagonal of J_surface^T (M_boundary tensor I) J_surface, without using
// a physical prepared state or the nonphysical volume extension.
mfem::Vector assembleSurfaceParameterMassDiagonal(
const fem::FEM &finiteElements,
const PreparedDomainDeformationRuntime &deformation
) {
if (!finiteElements.okay() || !deformation.matchesCurrentDiscretization() ||
!deformation.supportsSurfaceParameterMetric()) {
throw std::invalid_argument("A surface parameter metric requires a current reference surface tangent.");
}
const auto dependencies = deformation.discretizationDependencies();
if (dependencies.physicalMeshIdentity != finiteElements.mesh.get() ||
dependencies.surfaceScalarSpaceIdentity != finiteElements.surfaceDeformationFes.get() ||
dependencies.volumeDisplacementSpaceIdentity != finiteElements.displacementFes.get()) {
throw std::invalid_argument("The surface parameter metric and deformation must use the same FEM.");
}
int ranks = 0;
MPI_Comm_size(finiteElements.mesh->GetComm(), &ranks);
if (ranks != 1) {
throw std::invalid_argument("Surface parameter metric assembly currently requires one MPI rank.");
}
using Schema = utils::domain::CoreEnvelopeVacuumDomainSchema;
const auto surfaceMap =
field::make_stellar_surface_scalar_dof_map<Schema>(*finiteElements.surfaceDeformationFes);
const int dimension = deformation.descriptor().surfaceDeformation.spatialDimension;
if (deformation.surfaceDisplacementSize() != dimension * surfaceMap.local_size()) {
throw std::invalid_argument("The surface tangent and scalar boundary map have incompatible traces.");
}
constexpr int attribute = Schema::template boundary_attribute<utils::domain::StellarSurface>();
mfem::Array<int> marker(finiteElements.mesh->bdr_attributes.Max());
if (attribute <= 0 || attribute > marker.Size()) {
throw std::invalid_argument("The surface parameter metric requires the stellar boundary attribute.");
}
marker = 0;
marker[attribute - 1] = 1;
mfem::ParBilinearForm mass(finiteElements.surfaceDeformationFes.get());
mass.AddBoundaryIntegrator(new mfem::MassIntegrator(), marker);
mass.Assemble();
mass.Finalize();
std::unique_ptr<mfem::HypreParMatrix> matrix(mass.ParallelAssemble());
if (!matrix)
throw std::runtime_error("The reference surface mass failed to assemble.");
const int count = deformation.parameterCount();
mfem::Vector parameters(count), direction(count), diagonal(count);
mfem::Vector trace(deformation.surfaceDisplacementSize());
mfem::Vector scalarTrace(surfaceMap.local_size());
mfem::Vector ambient(surfaceMap.volume_true_dof_size()), action(ambient.Size());
parameters = 0.0;
for (int parameter = 0; parameter < count; ++parameter) {
direction = 0.0;
direction(parameter) = 1.0;
deformation.applySurfaceJacobian(parameters, direction, trace);
double entry = 0.0;
for (int component = 0; component < dimension; ++component) {
for (int node = 0; node < scalarTrace.Size(); ++node) {
scalarTrace(node) = trace(dimension * node + component);
}
ambient = 0.0;
surfaceMap.scatter(scalarTrace, ambient);
matrix->Mult(ambient, action);
entry += ambient * action;
}
if (!std::isfinite(entry) || entry <= 0.0) {
throw std::runtime_error("A surface parameter has a nonpositive or nonfinite reference metric.");
}
diagonal(parameter) = entry;
}
return diagonal;
}
} // namespace mean_field::deformation

View File

@@ -172,6 +172,51 @@ export namespace mean_field::deformation {
return m_surface.parameterCount();
}
[[nodiscard]] bool hasNodalRadialParameters() const noexcept {
if constexpr (requires { PreparedSurface::hasNodalRadialParameters; }) {
return PreparedSurface::hasNodalRadialParameters;
}
return false;
}
[[nodiscard]] bool supportsSurfaceParameterMetric() const noexcept {
if constexpr (requires { PreparedSurface::hasCanonicalNodalVectorTrace; }) {
return PreparedSurface::hasCanonicalNodalVectorTrace;
}
return false;
}
// Reference surface metrics need the prescription tangent before any
// physical state has been prepared. Exposing that tangent avoids
// making normalization depend on the arbitrary interior mesh lift.
void applySurfaceJacobian(
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
mfem::Vector &surfaceDirection
) const {
requireCurrentDiscretization();
requireParameterSize(parameters);
requireParameterSize(parameterDirection);
if (surfaceDirection.Size() != surfaceDisplacementSize()) {
throw std::invalid_argument("The surface tangent output has an incompatible trace size.");
}
m_surface.applyJacobian(parameters, parameterDirection, surfaceDirection);
}
void applySurfaceJacobianTranspose(
const mfem::Vector &parameters,
const mfem::Vector &surfaceDual,
mfem::Vector &parameterDual
) const {
requireCurrentDiscretization();
requireParameterSize(parameters);
requireParameterSize(parameterDual);
if (surfaceDual.Size() != surfaceDisplacementSize()) {
throw std::invalid_argument("The surface transpose input has an incompatible trace size.");
}
m_surface.applyJacobianTranspose(parameters, surfaceDual, parameterDual);
}
[[nodiscard]] int surfaceDisplacementSize() const noexcept {
return m_surface.surfaceDisplacementSize();
}
@@ -580,6 +625,30 @@ export namespace mean_field::deformation {
return m_implementation->parameterCount();
}
[[nodiscard]] bool hasNodalRadialParameters() const noexcept {
return m_implementation->hasNodalRadialParameters();
}
[[nodiscard]] bool supportsSurfaceParameterMetric() const noexcept {
return m_implementation->supportsSurfaceParameterMetric();
}
void applySurfaceJacobian(
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
mfem::Vector &surfaceDirection
) const {
m_implementation->applySurfaceJacobian(parameters, parameterDirection, surfaceDirection);
}
void applySurfaceJacobianTranspose(
const mfem::Vector &parameters,
const mfem::Vector &surfaceDual,
mfem::Vector &parameterDual
) const {
m_implementation->applySurfaceJacobianTranspose(parameters, surfaceDual, parameterDual);
}
[[nodiscard]] int surfaceDisplacementSize() const noexcept {
return m_implementation->surfaceDisplacementSize();
}
@@ -654,10 +723,22 @@ export namespace mean_field::deformation {
private:
class Interface {
public:
virtual ~Interface() = default;
virtual ~Interface() = default;
[[nodiscard]] virtual DomainDeformationDescriptor descriptor() const noexcept = 0;
[[nodiscard]] virtual int parameterCount() const noexcept = 0;
[[nodiscard]] virtual DomainDeformationDescriptor descriptor() const noexcept = 0;
[[nodiscard]] virtual int parameterCount() const noexcept = 0;
[[nodiscard]] virtual bool hasNodalRadialParameters() const noexcept = 0;
[[nodiscard]] virtual bool supportsSurfaceParameterMetric() const noexcept = 0;
virtual void applySurfaceJacobian(
const mfem::Vector &,
const mfem::Vector &,
mfem::Vector &
) const = 0;
virtual void applySurfaceJacobianTranspose(
const mfem::Vector &,
const mfem::Vector &,
mfem::Vector &
) const = 0;
[[nodiscard]] virtual int surfaceDisplacementSize() const noexcept = 0;
[[nodiscard]] virtual int volumeDisplacementSize() const noexcept = 0;
[[nodiscard]] virtual bool matchesCurrentDiscretization() const noexcept = 0;
@@ -705,6 +786,46 @@ export namespace mean_field::deformation {
[[nodiscard]] int parameterCount() const noexcept override {
return m_preparedDeformation.parameterCount();
}
[[nodiscard]] bool hasNodalRadialParameters() const noexcept override {
if constexpr (requires { m_preparedDeformation.hasNodalRadialParameters(); }) {
return m_preparedDeformation.hasNodalRadialParameters();
}
return false;
}
[[nodiscard]] bool supportsSurfaceParameterMetric() const noexcept override {
if constexpr (requires(const PreparedDeformation &prepared, const mfem::Vector &x, mfem::Vector &y) {
{ prepared.supportsSurfaceParameterMetric() } noexcept -> std::same_as<bool>;
{ prepared.applySurfaceJacobian(x, x, y) } -> std::same_as<void>;
{ prepared.applySurfaceJacobianTranspose(x, x, y) } -> std::same_as<void>;
}) {
return m_preparedDeformation.supportsSurfaceParameterMetric();
}
return false;
}
void applySurfaceJacobian(
const mfem::Vector &parameters,
const mfem::Vector &direction,
mfem::Vector &trace
) const override {
if constexpr (requires { m_preparedDeformation.applySurfaceJacobian(parameters, direction, trace); }) {
m_preparedDeformation.applySurfaceJacobian(parameters, direction, trace);
} else {
throw std::logic_error("This domain deformation does not expose its surface tangent.");
}
}
void applySurfaceJacobianTranspose(
const mfem::Vector &parameters,
const mfem::Vector &trace,
mfem::Vector &dual
) const override {
if constexpr (requires {
m_preparedDeformation.applySurfaceJacobianTranspose(parameters, trace, dual);
}) {
m_preparedDeformation.applySurfaceJacobianTranspose(parameters, trace, dual);
} else {
throw std::logic_error("This domain deformation does not expose its surface transpose.");
}
}
[[nodiscard]] int surfaceDisplacementSize() const noexcept override {
return m_preparedDeformation.surfaceDisplacementSize();
}

View File

@@ -44,6 +44,9 @@ export namespace mean_field::deformation {
class PreparedNodalRadialSurface final {
public:
static constexpr bool hasNodalRadialParameters = true;
// Trace entries follow dimension * canonical boundary-node index + component.
static constexpr bool hasCanonicalNodalVectorTrace = true;
[[nodiscard]] SurfaceDeformationDescriptor descriptor() const noexcept;
[[nodiscard]] int parameterCount() const noexcept;

View File

@@ -0,0 +1,140 @@
module;
#include <optional>
#include <span>
#include <vector>
#include <mfem/linalg/densemat.hpp>
export module mean_field:deformation.spherical_harmonic_surface;
export import :deformation.nodal_radial_surface;
export namespace mean_field::deformation {
struct SphericalHarmonicSurfaceOptions final {
int maximumDegree{4};
// A cutoff retains both real sine/cosine partners. Zero restricts the
// shape to axisymmetry; an omitted cutoff retains every order.
std::optional<int> maximumAzimuthalOrder{};
void validate() const;
};
struct SphericalHarmonicMode final {
int degree;
int order;
constexpr bool operator==(const SphericalHarmonicMode &) const = default;
};
class PreparedSphericalHarmonicSurface;
class SphericalHarmonicSurface final {
public:
using PreparedType = PreparedSphericalHarmonicSurface;
explicit SphericalHarmonicSurface(SphericalHarmonicSurfaceOptions options = {});
explicit SphericalHarmonicSurface(
mfem::Vector referenceCenter,
SphericalHarmonicSurfaceOptions options = {}
);
[[nodiscard]] const mfem::Vector &referenceCenter() const noexcept;
[[nodiscard]] const SphericalHarmonicSurfaceOptions &options() const noexcept;
[[nodiscard]] int modeCount() const noexcept;
[[nodiscard]] SurfaceDeformationDescriptor descriptor() const noexcept;
void validate() const;
private:
mfem::Vector m_referenceCenter;
SphericalHarmonicSurfaceOptions m_options;
};
// Coefficients have units of radial length. The fixed reference functions
// satisfy integral(Y_lm^2 dOmega)=1, with Condon--Shortley phase, negative
// orders denoting sine and positive orders cosine. FE interpolation of
// their nodal samples need not retain continuous spherical orthogonality.
class PreparedSphericalHarmonicSurface final {
public:
static constexpr bool hasNodalRadialParameters = false;
// Trace entries follow dimension * canonical boundary-node index + component.
static constexpr bool hasCanonicalNodalVectorTrace = true;
[[nodiscard]] SurfaceDeformationDescriptor descriptor() const noexcept;
[[nodiscard]] int parameterCount() const noexcept;
[[nodiscard]] long long globalParameterCount() const noexcept;
[[nodiscard]] long long globalParameterOffset() const noexcept;
[[nodiscard]] int spatialDimension() const noexcept;
[[nodiscard]] int surfaceNodeCount() const noexcept;
[[nodiscard]] int surfaceDisplacementSize() const noexcept;
[[nodiscard]] long long globalSurfaceDisplacementSize() const noexcept;
[[nodiscard]] long long globalSurfaceDisplacementOffset() const noexcept;
[[nodiscard]] int surfaceDisplacementDof(
int surfaceNode,
int component
) const;
[[nodiscard]] double radialDirection(
int surfaceNode,
int component
) const;
[[nodiscard]] double referenceRadius(int surfaceNode) const;
[[nodiscard]] const mfem::Vector &referenceCenter() const noexcept;
[[nodiscard]] const field::ScalarBoundaryDofMap &surfaceDofMap() const noexcept;
[[nodiscard]] std::span<const SphericalHarmonicMode> modes() const noexcept;
[[nodiscard]] const mfem::DenseMatrix &nodalBasis() const noexcept;
void buildSurfaceDisplacement(
const mfem::Vector &parameters,
mfem::Vector &surfaceDisplacement
) const;
void applyJacobian(
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
mfem::Vector &surfaceDisplacementDirection
) const;
void applyJacobianTranspose(
const mfem::Vector &parameters,
const mfem::Vector &surfaceDisplacementDual,
mfem::Vector &parameterDual
) const;
void applyPullbackDerivative(
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
const mfem::Vector &surfaceDisplacementDual,
mfem::Vector &parameterDualAction
) const;
private:
friend PreparedSphericalHarmonicSurface compileSurfaceDeformationPrescription(
const SphericalHarmonicSurface &,
const SurfaceDeformationCompilationContext &
);
PreparedSphericalHarmonicSurface(
PreparedNodalRadialSurface radialSurface,
std::vector<SphericalHarmonicMode> modes,
mfem::DenseMatrix nodalBasis
);
void requireParameters(const mfem::Vector &parameters) const;
void requireSurfaceSize(const mfem::Vector &surfaceDisplacement) const;
void applyEmbedding(
const mfem::Vector &parameters,
mfem::Vector &surfaceDisplacement
) const;
PreparedNodalRadialSurface m_radialSurface;
std::vector<SphericalHarmonicMode> m_modes;
mfem::DenseMatrix m_nodalBasis;
};
[[nodiscard]] PreparedSphericalHarmonicSurface compileSurfaceDeformationPrescription(
const SphericalHarmonicSurface &prescription,
const SurfaceDeformationCompilationContext &context
);
static_assert(SurfaceDeformationPrescription<SphericalHarmonicSurface>);
static_assert(PreparedSurfaceDeformationPrescription<PreparedSphericalHarmonicSurface>);
static_assert(SurfaceDeformationCompilable<
SphericalHarmonicSurface,
SurfaceDeformationCompilationContext>);
} // namespace mean_field::deformation

View File

@@ -0,0 +1,18 @@
module;
#include <mfem/linalg/vector.hpp>
export module mean_field:deformation.surface_metric;
export import :deformation.domain_deformation;
export import :fem;
export namespace mean_field::deformation {
// A reference parameter metric depends on the surface tangent, not the
// interior mesh lift. Assembly is out of line to keep MFEM quadrature
// implementation headers out of every normalization/preconditioner user.
[[nodiscard]] mfem::Vector assembleSurfaceParameterMassDiagonal(
const fem::FEM &finiteElements,
const PreparedDomainDeformationRuntime &deformation
);
} // namespace mean_field::deformation

View File

@@ -6,10 +6,12 @@ module;
#include <type_traits>
#include <utility>
#include <mfem/linalg/vector.hpp>
#include <mpi.h>
export module mean_field:equilibrium.stellar_discretization;
export import :deformation.domain_deformation;
export import :fem;
export import :mapping.domain_mapper;
export import :normalization.physical_riesz;
@@ -26,14 +28,40 @@ export namespace mean_field::equilibrium {
* references retained by prepared operators. The mapper is part of that
* owned FEM and therefore has the same lifetime.
*/
template <normalization::NormalizationPrescription Normalization> class StellarDiscretizationFor final {
template <
normalization::NormalizationPrescription Normalization,
deformation::SurfaceDeformationPrescription SurfaceDeformation = deformation::NodalRadialSurface,
deformation::InteriorDeformationExtension InteriorDeformation = deformation::PowerLawRadialInteriorExtension,
deformation::VacuumDeformationExtension VacuumDeformation = deformation::FixedInfinityRadialVacuumExtension>
requires deformation::SurfaceDeformationCompilable<
SurfaceDeformation,
deformation::SurfaceDeformationCompilationContext> &&
deformation::InteriorDeformationExtensionCompilable<
InteriorDeformation,
deformation::RadialDeformationExtensionCompilationContext> &&
deformation::VacuumDeformationExtensionCompilable<
VacuumDeformation,
deformation::RadialDeformationExtensionCompilationContext>
class StellarDiscretizationFor final {
public:
using NormalizationPrescriptionType = std::remove_cvref_t<Normalization>;
using NormalizationPrescriptionType = std::remove_cvref_t<Normalization>;
using SurfaceDeformationPrescriptionType = SurfaceDeformation;
using InteriorDeformationExtensionType = InteriorDeformation;
using VacuumDeformationExtensionType = VacuumDeformation;
explicit StellarDiscretizationFor(fem::FEM &&finiteElementModel)
requires std::same_as<
NormalizationPrescriptionType,
normalization::Unnormalized>
NormalizationPrescriptionType,
normalization::Unnormalized> &&
std::same_as<
SurfaceDeformation,
deformation::NodalRadialSurface> &&
std::same_as<
InteriorDeformation,
deformation::PowerLawRadialInteriorExtension> &&
std::same_as<
VacuumDeformation,
deformation::FixedInfinityRadialVacuumExtension>
: StellarDiscretizationFor(
std::move(finiteElementModel),
normalization::Unnormalized{}
@@ -49,9 +77,55 @@ export namespace mean_field::equilibrium {
StellarDiscretizationFor(
fem::FEM &&finiteElementModel,
NormalizationPrescriptionType normalizationPrescription
)
requires std::same_as<
SurfaceDeformation,
deformation::NodalRadialSurface> &&
std::same_as<
InteriorDeformation,
deformation::PowerLawRadialInteriorExtension> &&
std::same_as<
VacuumDeformation,
deformation::FixedInfinityRadialVacuumExtension>
: StellarDiscretizationFor(
std::move(finiteElementModel),
std::move(normalizationPrescription),
MakeDefaultSurface(finiteElementModel),
InteriorDeformation{},
VacuumDeformation{}
) {
}
StellarDiscretizationFor(
fem::FEM &&finiteElementModel,
NormalizationPrescriptionType normalizationPrescription,
SurfaceDeformation surfaceDeformation
)
requires std::default_initializable<InteriorDeformation> && std::default_initializable<VacuumDeformation>
: StellarDiscretizationFor(
std::move(finiteElementModel),
std::move(normalizationPrescription),
std::move(surfaceDeformation),
InteriorDeformation{},
VacuumDeformation{}
) {
}
StellarDiscretizationFor(
fem::FEM &&finiteElementModel,
NormalizationPrescriptionType normalizationPrescription,
SurfaceDeformation surfaceDeformation,
InteriorDeformation interiorDeformation,
VacuumDeformation vacuumDeformation
)
: m_finiteElementModel(TakeOwnership(std::move(finiteElementModel))),
m_normalizationPrescription(std::move(normalizationPrescription)) {
m_normalizationPrescription(std::move(normalizationPrescription)),
m_surfaceDeformation(std::move(surfaceDeformation)),
m_interiorDeformation(std::move(interiorDeformation)),
m_vacuumDeformation(std::move(vacuumDeformation)) {
m_surfaceDeformation.validate();
m_interiorDeformation.validate();
m_vacuumDeformation.validate();
}
StellarDiscretizationFor(
@@ -90,6 +164,21 @@ export namespace mean_field::equilibrium {
[[nodiscard]] const NormalizationPrescriptionType &normalizationPrescription() const && = delete;
[[nodiscard]] const SurfaceDeformation &surfaceDeformationPrescription() const & noexcept {
return m_surfaceDeformation;
}
[[nodiscard]] const SurfaceDeformation &surfaceDeformationPrescription() const && = delete;
[[nodiscard]] const InteriorDeformation &interiorDeformationExtension() const & noexcept {
return m_interiorDeformation;
}
[[nodiscard]] const InteriorDeformation &interiorDeformationExtension() const && = delete;
[[nodiscard]] const VacuumDeformation &vacuumDeformationExtension() const & noexcept {
return m_vacuumDeformation;
}
[[nodiscard]] const VacuumDeformation &vacuumDeformationExtension() const && = delete;
[[nodiscard]] bool isCurrent() const noexcept {
return m_finiteElementModel != nullptr && m_finiteElementModel->okay();
}
@@ -108,6 +197,15 @@ export namespace mean_field::equilibrium {
return *m_finiteElementModel;
}
[[nodiscard]] static deformation::NodalRadialSurface MakeDefaultSurface(const fem::FEM &finiteElementModel) {
if (!finiteElementModel.okay()) {
throw std::invalid_argument("A default surface requires a complete finite-element model.");
}
mfem::Vector center(finiteElementModel.mesh->SpaceDimension());
center = 0.0;
return deformation::NodalRadialSurface{std::move(center)};
}
[[nodiscard]] static std::unique_ptr<fem::FEM> TakeOwnership(fem::FEM &&finiteElementModel) {
if (!finiteElementModel.okay()) {
throw std::invalid_argument("A stellar discretization requires a complete finite-element model.");
@@ -117,6 +215,9 @@ export namespace mean_field::equilibrium {
std::unique_ptr<fem::FEM> m_finiteElementModel;
NormalizationPrescriptionType m_normalizationPrescription;
SurfaceDeformation m_surfaceDeformation;
InteriorDeformation m_interiorDeformation;
VacuumDeformation m_vacuumDeformation;
};
template <normalization::NormalizationPrescription Normalization>
@@ -125,12 +226,43 @@ export namespace mean_field::equilibrium {
Normalization
) -> StellarDiscretizationFor<std::remove_cvref_t<Normalization>>;
template <
typename Normalization,
typename Surface>
StellarDiscretizationFor(
fem::FEM &&,
Normalization,
Surface
)
-> StellarDiscretizationFor<
std::remove_cvref_t<Normalization>,
std::remove_cvref_t<Surface>>;
template <
typename Normalization,
typename Surface,
typename Interior,
typename Vacuum>
StellarDiscretizationFor(
fem::FEM &&,
Normalization,
Surface,
Interior,
Vacuum
)
-> StellarDiscretizationFor<
std::remove_cvref_t<Normalization>,
std::remove_cvref_t<Surface>,
std::remove_cvref_t<Interior>,
std::remove_cvref_t<Vacuum>>;
using StellarDiscretization = StellarDiscretizationFor<normalization::Unnormalized>;
template <typename Candidate> struct IsStellarDiscretization : std::false_type { };
template <normalization::NormalizationPrescription Normalization>
struct IsStellarDiscretization<StellarDiscretizationFor<Normalization>> : std::true_type { };
template <typename Normalization, typename Surface, typename Interior, typename Vacuum>
struct IsStellarDiscretization<StellarDiscretizationFor<Normalization, Surface, Interior, Vacuum>>
: std::true_type { };
template <typename Candidate>
concept StellarDiscretizationType = IsStellarDiscretization<std::remove_cvref_t<Candidate>>::value;
@@ -145,6 +277,51 @@ export namespace mean_field::equilibrium {
};
}
// Geometry is a numerical prescription. Keeping it on the discretization
// leaves the model's EOS, surface pressure and integral constraints intact.
template <
normalization::NormalizationPrescription Normalization,
deformation::SurfaceDeformationPrescription Surface>
requires deformation::SurfaceDeformationCompilable<
Surface,
deformation::SurfaceDeformationCompilationContext>
[[nodiscard]] auto makeStellarDiscretization(
fem::FEM &&finiteElementModel,
Normalization normalizationPrescription,
Surface surface
) {
return StellarDiscretizationFor<Normalization, Surface>{
std::move(finiteElementModel), std::move(normalizationPrescription), std::move(surface)
};
}
template <
normalization::NormalizationPrescription Normalization,
deformation::SurfaceDeformationPrescription Surface,
deformation::InteriorDeformationExtension Interior,
deformation::VacuumDeformationExtension Vacuum>
requires deformation::SurfaceDeformationCompilable<
Surface,
deformation::SurfaceDeformationCompilationContext> &&
deformation::InteriorDeformationExtensionCompilable<
Interior,
deformation::RadialDeformationExtensionCompilationContext> &&
deformation::VacuumDeformationExtensionCompilable<
Vacuum,
deformation::RadialDeformationExtensionCompilationContext>
[[nodiscard]] auto makeStellarDiscretization(
fem::FEM &&finiteElementModel,
Normalization normalizationPrescription,
Surface surface,
Interior interior,
Vacuum vacuum
) {
return StellarDiscretizationFor<Normalization, Surface, Interior, Vacuum>{
std::move(finiteElementModel), std::move(normalizationPrescription), std::move(surface),
std::move(interior), std::move(vacuum)
};
}
template <normalization::NormalizationPrescription Normalization>
StellarDiscretizationFor<std::remove_cvref_t<Normalization>> makeStellarDiscretization(
fem::FEM &,

View File

@@ -83,10 +83,12 @@ export import :material.thermodynamic_equations;
export import :deformation.descriptors;
export import :deformation.surface_prescription;
export import :deformation.nodal_radial_surface;
export import :deformation.spherical_harmonic_surface;
export import :deformation.interior_extension;
export import :deformation.vacuum_extension;
export import :deformation.radial_extensions;
export import :deformation.domain_deformation;
export import :deformation.surface_metric;
export import :deformation.safe_newton_step;
export import :model.stellar;
export import :operators.root_manifest;

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.");
}

View File

@@ -154,7 +154,10 @@ export namespace mean_field::equilibrium {
m_discretization.domainMapper(),
m_stellarModel,
operators::PressureSurfaceConstraintView{m_compiledSurfaceConstraint},
CompileDefaultDomainDeformation(finiteElementModel)
CompileDomainDeformation(
m_discretization,
finiteElementModel
)
) {
VerifyProblem();
}
@@ -313,19 +316,13 @@ export namespace mean_field::equilibrium {
);
}
[[nodiscard]] static deformation::PreparedDomainDeformationRuntime
CompileDefaultDomainDeformation(fem::FEM &finiteElementModel) {
MFEM_VERIFY(
finiteElementModel.mesh != nullptr,
"Default stellar domain-deformation compilation requires a physical mesh."
);
mfem::Vector referenceCenter(finiteElementModel.mesh->SpaceDimension());
referenceCenter = 0.0;
[[nodiscard]] static deformation::PreparedDomainDeformationRuntime CompileDomainDeformation(
const DiscretizationType &discretization,
fem::FEM &finiteElementModel
) {
return deformation::PreparedDomainDeformationRuntime{deformation::compileDomainDeformation(
deformation::NodalRadialSurface{std::move(referenceCenter)},
deformation::PowerLawRadialInteriorExtension{}, deformation::FixedInfinityRadialVacuumExtension{},
finiteElementModel
discretization.surfaceDeformationPrescription(), discretization.interiorDeformationExtension(),
discretization.vacuumDeformationExtension(), finiteElementModel
)};
}

View File

@@ -17,6 +17,8 @@ module;
export module mean_field:preconditioning.material_surface;
import :deformation.surface_metric;
export import :operators.prepared_stellar_equilibrium;
export import :operators.stellar_equilibrium_problem;
export import :preconditioning.backend_implementations;
@@ -1440,6 +1442,14 @@ export namespace mean_field::preconditioning {
using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema;
const fem::FEM &finiteElements = operation.GetHydrostaticOperator().GetFEM();
if (!operation.GetDomainDeformation().hasNodalRadialParameters()) {
// The inverse acts on the parameter dual, not an ambient
// nodal trace. Keep the existing diagonal approximation but
// form it in the prescription's actual coordinate space.
return deformation::assembleSurfaceParameterMassDiagonal(
finiteElements, operation.GetDomainDeformation()
);
}
MFEM_VERIFY(
finiteElements.mesh != nullptr && finiteElements.surfaceDeformationFes != nullptr,
"The material/free-surface Riesz surrogate requires the surface finite-element space."
@@ -1492,10 +1502,26 @@ export namespace mean_field::preconditioning {
using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema;
const fem::FEM &finiteElements = m_operation->GetHydrostaticOperator().GetFEM();
const field::ScalarBoundaryDofMap surfaceMap =
field::make_stellar_surface_scalar_dof_map<DomainSchema>(*finiteElements.surfaceDeformationFes);
if (surfaceMap.local_size() != m_surfaceDiagonal.Size()) {
throw std::logic_error("Surface Riesz calibration received an incompatible boundary coordinate map.");
const bool nodal = m_operation->GetDomainDeformation().hasNodalRadialParameters();
field::ScalarBoundaryDofMap surfaceMap;
if (nodal) {
surfaceMap =
field::make_stellar_surface_scalar_dof_map<DomainSchema>(*finiteElements.surfaceDeformationFes);
if (surfaceMap.local_size() != m_surfaceDiagonal.Size()) {
throw std::logic_error(
"Surface Riesz calibration received an incompatible boundary coordinate map."
);
}
} else {
int ranks = 0;
if (MPI_Comm_size(Communicator(*m_operation), &ranks) != MPI_SUCCESS || ranks != 1) {
throw std::invalid_argument(
"Surface parameter calibration currently requires one MPI rank for nonnodal coordinates."
);
}
if (m_surfaceDiagonal.Size() != m_operation->GetDomainDeformation().parameterCount()) {
throw std::logic_error("Surface Riesz calibration received an incompatible parameter metric.");
}
}
mfem::Vector probe(m_surfaceDiagonal.Size());
@@ -1513,7 +1539,11 @@ export namespace mean_field::preconditioning {
double localDenominator = 0.0;
for (int sample = 0; sample < calibration.probeCount; ++sample) {
for (int index = 0; index < probe.Size(); ++index) {
std::uint64_t value = static_cast<std::uint64_t>(surfaceMap.global_boundary_dof(index));
// Preserve nodal probes exactly. Nonnodal serial probes
// use the immutable ordered parameter index instead of
// pretending that each coefficient owns a boundary node.
std::uint64_t value =
static_cast<std::uint64_t>(nodal ? surfaceMap.global_boundary_dof(index) : index);
value += 0x9e3779b97f4a7c15ULL * static_cast<std::uint64_t>(sample + 1);
value = (value ^ (value >> 30U)) * 0xbf58476d1ce4e5b9ULL;
value = (value ^ (value >> 27U)) * 0x94d049bb133111ebULL;
@@ -1871,6 +1901,12 @@ export namespace mean_field::preconditioning {
[[nodiscard]] static field::ScalarBoundaryDofMap
SurfaceMap(const operators::PreparedStellarEquilibriumOperator &operation) {
if (!operation.GetDomainDeformation().hasNodalRadialParameters()) {
throw std::invalid_argument(
"SurfaceH1MassStiffness requires nodal radial parameters; a parameter-space H1 surrogate "
"has not been implemented for this surface prescription."
);
}
using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema;
const fem::FEM &finiteElements = operation.GetHydrostaticOperator().GetFEM();
return field::make_stellar_surface_scalar_dof_map<DomainSchema>(*finiteElements.surfaceDeformationFes);