module; #include #include #include #include #include #include #include #include #include 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(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 evaluateModes( const double x, const double y, const double z, const int maximumDegree, const int maximumOrder, const std::vector &modes ) { const std::size_t stride = static_cast(maximumOrder) + 1; std::vector legendre((static_cast(maximumDegree) + 1) * stride, 0.); const auto p = [&](int l, int m) -> double & { return legendre[static_cast(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 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(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::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(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 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(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 PreparedSphericalHarmonicSurface::modes() const noexcept { return m_modes; } const mfem::DenseMatrix &PreparedSphericalHarmonicSurface::nodalBasis() const noexcept { return m_nodalBasis; } void PreparedSphericalHarmonicSurface::requireParameters(const mfem::Vector ¶meters) 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 ¶meters, 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(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(radialAmplitude) * radialDirection(node, component)); } } void PreparedSphericalHarmonicSurface::buildSurfaceDisplacement( const mfem::Vector ¶meters, mfem::Vector &surfaceDisplacement ) const { requireParameters(parameters); requireSurfaceSize(surfaceDisplacement); applyEmbedding(parameters, surfaceDisplacement); } void PreparedSphericalHarmonicSurface::applyJacobian( const mfem::Vector ¶meters, const mfem::Vector ¶meterDirection, mfem::Vector &surfaceDisplacementDirection ) const { requireParameters(parameters); requireParameters(parameterDirection); requireSurfaceSize(surfaceDisplacementDirection); applyEmbedding(parameterDirection, surfaceDisplacementDirection); } void PreparedSphericalHarmonicSurface::applyJacobianTranspose( const mfem::Vector ¶meters, const mfem::Vector &surfaceDisplacementDual, mfem::Vector ¶meterDual ) 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(radialDirection(node, component)) * surfaceDisplacementDual(surfaceDisplacementDof(node, component)); work += static_cast(m_nodalBasis(node, mode)) * radialWork; } parameterDual(mode) = checkedDouble(work); } } void PreparedSphericalHarmonicSurface::applyPullbackDerivative( const mfem::Vector ¶meters, const mfem::Vector ¶meterDirection, const mfem::Vector &surfaceDisplacementDual, mfem::Vector ¶meterDualAction ) 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 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