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

@@ -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