Add native spherical harmonic surface coordinates to coupled equilibrium
This commit is contained in:
381
libmeanfield/impl/deformation/spherical_harmonic_surface.cpp
Normal file
381
libmeanfield/impl/deformation/spherical_harmonic_surface.cpp
Normal 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 ¶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<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 ¶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<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 ¶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<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
|
||||
89
libmeanfield/impl/deformation/surface_metric.cpp
Normal file
89
libmeanfield/impl/deformation/surface_metric.cpp
Normal 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
|
||||
@@ -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 ¶meters,
|
||||
const mfem::Vector ¶meterDirection,
|
||||
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 ¶meters,
|
||||
const mfem::Vector &surfaceDual,
|
||||
mfem::Vector ¶meterDual
|
||||
) 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 ¶meters,
|
||||
const mfem::Vector ¶meterDirection,
|
||||
mfem::Vector &surfaceDirection
|
||||
) const {
|
||||
m_implementation->applySurfaceJacobian(parameters, parameterDirection, surfaceDirection);
|
||||
}
|
||||
|
||||
void applySurfaceJacobianTranspose(
|
||||
const mfem::Vector ¶meters,
|
||||
const mfem::Vector &surfaceDual,
|
||||
mfem::Vector ¶meterDual
|
||||
) 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 ¶meters,
|
||||
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 ¶meters,
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 ¶meters,
|
||||
mfem::Vector &surfaceDisplacement
|
||||
) const;
|
||||
void applyJacobian(
|
||||
const mfem::Vector ¶meters,
|
||||
const mfem::Vector ¶meterDirection,
|
||||
mfem::Vector &surfaceDisplacementDirection
|
||||
) const;
|
||||
void applyJacobianTranspose(
|
||||
const mfem::Vector ¶meters,
|
||||
const mfem::Vector &surfaceDisplacementDual,
|
||||
mfem::Vector ¶meterDual
|
||||
) const;
|
||||
void applyPullbackDerivative(
|
||||
const mfem::Vector ¶meters,
|
||||
const mfem::Vector ¶meterDirection,
|
||||
const mfem::Vector &surfaceDisplacementDual,
|
||||
mfem::Vector ¶meterDualAction
|
||||
) const;
|
||||
|
||||
private:
|
||||
friend PreparedSphericalHarmonicSurface compileSurfaceDeformationPrescription(
|
||||
const SphericalHarmonicSurface &,
|
||||
const SurfaceDeformationCompilationContext &
|
||||
);
|
||||
|
||||
PreparedSphericalHarmonicSurface(
|
||||
PreparedNodalRadialSurface radialSurface,
|
||||
std::vector<SphericalHarmonicMode> modes,
|
||||
mfem::DenseMatrix nodalBasis
|
||||
);
|
||||
void requireParameters(const mfem::Vector ¶meters) const;
|
||||
void requireSurfaceSize(const mfem::Vector &surfaceDisplacement) const;
|
||||
void applyEmbedding(
|
||||
const mfem::Vector ¶meters,
|
||||
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
|
||||
18
libmeanfield/interface/deformation/surface_metric.cppm
Normal file
18
libmeanfield/interface/deformation/surface_metric.cppm
Normal 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
|
||||
@@ -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 &,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user