2026-08-30 16:41:14 -04:00
|
|
|
module;
|
|
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <array>
|
|
|
|
|
#include <cmath>
|
|
|
|
|
#include <cstdint>
|
|
|
|
|
#include <iostream>
|
|
|
|
|
#include <limits>
|
|
|
|
|
#include <string>
|
|
|
|
|
#include <utility>
|
2026-09-01 11:50:13 -04:00
|
|
|
#include <vector>
|
2026-08-30 16:41:14 -04:00
|
|
|
|
|
|
|
|
#include <mfem.hpp>
|
|
|
|
|
#include <mpi.h>
|
|
|
|
|
|
|
|
|
|
export module experiment.stellar_null_space;
|
|
|
|
|
|
|
|
|
|
import mean_field;
|
|
|
|
|
import test_helpers;
|
|
|
|
|
|
|
|
|
|
export namespace experiment::null_space {
|
2026-09-01 11:50:13 -04:00
|
|
|
using Form = mean_field::utils::blocks::surface_deformed_stellar_equilibrium_form;
|
2026-08-30 16:41:14 -04:00
|
|
|
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
|
|
|
|
|
using Model = mean_field::models::StellarModel<mean_field::models::structure::PolytropicStructure>;
|
|
|
|
|
|
|
|
|
|
constexpr auto densityValue =
|
|
|
|
|
mean_field::utils::blocks::get_value_block<Form>(mean_field::utils::blocks::density_field.mass_term);
|
2026-09-01 11:50:13 -04:00
|
|
|
constexpr auto surfaceDeformationValue = mean_field::utils::blocks::get_value_block<Form>(
|
|
|
|
|
mean_field::utils::blocks::surface_deformation_field.parameters_term
|
|
|
|
|
);
|
2026-08-30 16:41:14 -04:00
|
|
|
constexpr auto gravityGradientValue =
|
|
|
|
|
mean_field::utils::blocks::get_value_block<Form>(mean_field::utils::blocks::gravity_field.gradient_term);
|
|
|
|
|
constexpr auto gravityPotentialValue =
|
|
|
|
|
mean_field::utils::blocks::get_value_block<Form>(mean_field::utils::blocks::gravity_field.poisson_term);
|
|
|
|
|
constexpr auto enthalpyValue =
|
|
|
|
|
mean_field::utils::blocks::get_value_block<Form>(mean_field::utils::blocks::enthalpy_field.specific_term);
|
|
|
|
|
constexpr auto bernoulliValue = mean_field::utils::blocks::get_value_block<Form>(
|
|
|
|
|
mean_field::utils::blocks::barotropic_constant_field.mass_normalization_term
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
constexpr auto gravityGradientResidual =
|
|
|
|
|
mean_field::utils::blocks::get_residual_block<Form>(mean_field::utils::blocks::gravity_field.gradient_term);
|
|
|
|
|
constexpr auto gravityPotentialResidual =
|
|
|
|
|
mean_field::utils::blocks::get_residual_block<Form>(mean_field::utils::blocks::gravity_field.poisson_term);
|
|
|
|
|
constexpr auto densityResidual =
|
|
|
|
|
mean_field::utils::blocks::get_residual_block<Form>(mean_field::utils::blocks::density_field.mass_term);
|
2026-09-01 11:50:13 -04:00
|
|
|
constexpr auto surfaceShapeResidual = mean_field::utils::blocks::get_residual_block<Form>(
|
|
|
|
|
mean_field::utils::blocks::surface_deformation_field.shape_equilibrium_term
|
2026-08-30 16:41:14 -04:00
|
|
|
);
|
|
|
|
|
constexpr auto enthalpyResidual =
|
|
|
|
|
mean_field::utils::blocks::get_residual_block<Form>(mean_field::utils::blocks::enthalpy_field.specific_term);
|
|
|
|
|
constexpr auto massResidual = mean_field::utils::blocks::get_residual_block<Form>(
|
|
|
|
|
mean_field::utils::blocks::barotropic_constant_field.mass_normalization_term
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
inline constexpr std::array<const char *, 6> residualBlockNames{"gravity_gradient", "gravity_potential", "closure",
|
2026-09-01 11:50:13 -04:00
|
|
|
"surface_shape", "hydrostatic", "mass"};
|
2026-08-30 16:41:14 -04:00
|
|
|
|
|
|
|
|
template <int index>
|
|
|
|
|
[[nodiscard]] mfem::Vector value_view(
|
|
|
|
|
mfem::Vector &vector,
|
|
|
|
|
const mean_field::operators::StellarEquilibriumLayout &layout,
|
|
|
|
|
const mean_field::utils::blocks::value_block<index> block
|
|
|
|
|
) {
|
|
|
|
|
return mfem::Vector(vector.GetData() + layout.offset(block), layout.size(block));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template <int index>
|
|
|
|
|
[[nodiscard]] mfem::Vector const_value_view(
|
|
|
|
|
const mfem::Vector &vector,
|
|
|
|
|
const mean_field::operators::StellarEquilibriumLayout &layout,
|
|
|
|
|
const mean_field::utils::blocks::value_block<index> block
|
|
|
|
|
) {
|
|
|
|
|
return mfem::Vector(const_cast<mfem::real_t *>(vector.GetData()) + layout.offset(block), layout.size(block));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template <int index>
|
|
|
|
|
[[nodiscard]] mfem::Vector residual_view(
|
|
|
|
|
mfem::Vector &vector,
|
|
|
|
|
const mean_field::operators::StellarEquilibriumLayout &layout,
|
|
|
|
|
const mean_field::utils::blocks::residual_block<index> block
|
|
|
|
|
) {
|
|
|
|
|
return mfem::Vector(vector.GetData() + layout.offset(block), layout.size(block));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template <int index>
|
|
|
|
|
[[nodiscard]] mfem::Vector const_residual_view(
|
|
|
|
|
const mfem::Vector &vector,
|
|
|
|
|
const mean_field::operators::StellarEquilibriumLayout &layout,
|
|
|
|
|
const mean_field::utils::blocks::residual_block<index> block
|
|
|
|
|
) {
|
|
|
|
|
return mfem::Vector(const_cast<mfem::real_t *>(vector.GetData()) + layout.offset(block), layout.size(block));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template <int index>
|
|
|
|
|
void assign_value_block(
|
|
|
|
|
mfem::Vector &vector,
|
|
|
|
|
const mean_field::operators::StellarEquilibriumLayout &layout,
|
|
|
|
|
const mean_field::utils::blocks::value_block<index> block,
|
|
|
|
|
const mfem::Vector &source
|
|
|
|
|
) {
|
2026-09-01 11:50:13 -04:00
|
|
|
MFEM_VERIFY(
|
|
|
|
|
source.Size() == layout.size(block), "Surface-mode experiment received a block with the wrong size."
|
|
|
|
|
);
|
2026-08-30 16:41:14 -04:00
|
|
|
value_view(vector, layout, block) = source;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[[nodiscard]] inline double global_norm(
|
|
|
|
|
const mfem::Vector &vector,
|
|
|
|
|
const MPI_Comm communicator
|
|
|
|
|
) {
|
|
|
|
|
const double localNormSquared = vector * vector;
|
|
|
|
|
double globalNormSquared = 0.0;
|
|
|
|
|
MPI_Allreduce(&localNormSquared, &globalNormSquared, 1, MPI_DOUBLE, MPI_SUM, communicator);
|
|
|
|
|
return std::sqrt(globalNormSquared);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inline void report_progress(
|
|
|
|
|
const MPI_Comm communicator,
|
|
|
|
|
const std::string &message
|
|
|
|
|
) {
|
|
|
|
|
int rank = 0;
|
|
|
|
|
MPI_Comm_rank(communicator, &rank);
|
|
|
|
|
if (rank == 0) {
|
2026-09-01 11:50:13 -04:00
|
|
|
std::cout << "[reduced-surface experiment] " << message << std::endl;
|
2026-08-30 16:41:14 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[[nodiscard]] inline mean_field::operators::StellarEquilibriumDependencies make_dependencies() {
|
|
|
|
|
return {
|
2026-09-01 11:50:13 -04:00
|
|
|
.discretization = {.identity = 2003, .revision = 1},
|
|
|
|
|
.density = {.identity = 2011, .revision = 1},
|
|
|
|
|
.surfaceDeformation = {.identity = 2017, .revision = 1},
|
|
|
|
|
.gravityGradient = {.identity = 2027, .revision = 1},
|
|
|
|
|
.gravityPotential = {.identity = 2029, .revision = 1},
|
|
|
|
|
.enthalpy = {.identity = 2039, .revision = 1},
|
|
|
|
|
.bernoulliConstant = {.identity = 2053, .revision = 1},
|
|
|
|
|
.rotation = {.identity = 2063, .revision = 1},
|
|
|
|
|
.targetMass = {.identity = 2069, .revision = 1}
|
2026-08-30 16:41:14 -04:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inline void increment_state_revisions(mean_field::operators::StellarEquilibriumDependencies &dependencies) {
|
|
|
|
|
++dependencies.density.revision;
|
2026-09-01 11:50:13 -04:00
|
|
|
++dependencies.surfaceDeformation.revision;
|
2026-08-30 16:41:14 -04:00
|
|
|
++dependencies.gravityGradient.revision;
|
|
|
|
|
++dependencies.gravityPotential.revision;
|
|
|
|
|
++dependencies.enthalpy.revision;
|
|
|
|
|
++dependencies.bernoulliConstant.revision;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[[nodiscard]] inline mfem::Vector pack_gravity_state(
|
|
|
|
|
const mfem::Vector &density,
|
|
|
|
|
const mfem::Vector &displacement,
|
|
|
|
|
const mfem::Vector &gravityGradient,
|
|
|
|
|
const mfem::Vector &gravityPotential
|
|
|
|
|
) {
|
|
|
|
|
const std::array<int, 5> offsets{
|
|
|
|
|
0, density.Size(), density.Size() + displacement.Size(),
|
|
|
|
|
density.Size() + displacement.Size() + gravityGradient.Size(),
|
|
|
|
|
density.Size() + displacement.Size() + gravityGradient.Size() + gravityPotential.Size()
|
|
|
|
|
};
|
|
|
|
|
mfem::Vector packed(offsets.back());
|
|
|
|
|
mfem::Vector(packed.GetData() + offsets[0], density.Size()) = density;
|
|
|
|
|
mfem::Vector(packed.GetData() + offsets[1], displacement.Size()) = displacement;
|
|
|
|
|
mfem::Vector(packed.GetData() + offsets[2], gravityGradient.Size()) = gravityGradient;
|
|
|
|
|
mfem::Vector(packed.GetData() + offsets[3], gravityPotential.Size()) = gravityPotential;
|
|
|
|
|
return packed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[[nodiscard]] inline Model make_model() {
|
|
|
|
|
const double pi = std::acos(-1.0);
|
|
|
|
|
const double targetMass = mean_field::utils::MASS;
|
|
|
|
|
constexpr double dimensionlessMass = 2.0182359509662283534;
|
|
|
|
|
const double polytropicConstant =
|
|
|
|
|
pi * mean_field::utils::G * std::pow(targetMass / (4.0 * pi * dimensionlessMass), 2.0 / 3.0);
|
|
|
|
|
|
|
|
|
|
return Model{
|
|
|
|
|
mean_field::models::structure::PolytropicStructure{
|
|
|
|
|
mean_field::eos::Polytrope{3.0, polytropicConstant}, targetMass
|
|
|
|
|
},
|
|
|
|
|
mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.0}}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class N3Equilibrium final {
|
|
|
|
|
public:
|
|
|
|
|
explicit N3Equilibrium(mean_field::utils::Args args)
|
|
|
|
|
: m_args(std::move(args)),
|
|
|
|
|
m_fem(
|
|
|
|
|
mean_field::fem::setup_fem(
|
|
|
|
|
m_args.mesh_file,
|
|
|
|
|
m_args,
|
|
|
|
|
0
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
m_model(make_model()),
|
|
|
|
|
m_operator(
|
|
|
|
|
m_fem,
|
|
|
|
|
*m_fem.domainMapperStateless,
|
|
|
|
|
m_model
|
|
|
|
|
),
|
|
|
|
|
m_state(m_operator.GetLayout().value_offsets().Last()),
|
|
|
|
|
m_dependencies(make_dependencies()) {
|
|
|
|
|
MFEM_VERIFY(m_fem.okay(), "The null-space experiment could not construct the finite-element problem.");
|
|
|
|
|
m_state = 0.0;
|
|
|
|
|
initialize_state();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[[nodiscard]] mean_field::fem::FEM &fem() noexcept {
|
|
|
|
|
return m_fem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[[nodiscard]] const mean_field::fem::FEM &fem() const noexcept {
|
|
|
|
|
return m_fem;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-01 11:50:13 -04:00
|
|
|
[[nodiscard]] Model &model() noexcept {
|
|
|
|
|
return m_model;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-30 16:41:14 -04:00
|
|
|
[[nodiscard]] mean_field::operators::PreparedStellarEquilibriumOperator &stellar_operator() noexcept {
|
|
|
|
|
return m_operator;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[[nodiscard]] const mean_field::operators::PreparedStellarEquilibriumOperator &
|
|
|
|
|
stellar_operator() const noexcept {
|
|
|
|
|
return m_operator;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[[nodiscard]] const mfem::Vector &state() const noexcept {
|
|
|
|
|
return m_state;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[[nodiscard]] mean_field::physics::RigidRotation rotation(const double fractionOfKeplerian) const {
|
|
|
|
|
const double radius = mean_field::utils::RADIUS;
|
|
|
|
|
const double mass = mean_field::utils::MASS;
|
|
|
|
|
const double keplerianSpeed = std::sqrt(mean_field::utils::G * mass / (radius * radius * radius));
|
|
|
|
|
|
|
|
|
|
mfem::Vector angularVelocity(3);
|
|
|
|
|
angularVelocity = 0.0;
|
|
|
|
|
angularVelocity(2) = fractionOfKeplerian * keplerianSpeed;
|
|
|
|
|
|
|
|
|
|
mfem::Vector center(3);
|
|
|
|
|
center = 0.0;
|
|
|
|
|
return mean_field::physics::RigidRotation(angularVelocity, center);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void prepare(
|
|
|
|
|
const mfem::Vector &state,
|
|
|
|
|
const mean_field::physics::RigidRotation &rotation
|
|
|
|
|
) {
|
|
|
|
|
m_currentState = state;
|
|
|
|
|
increment_state_revisions(m_dependencies);
|
|
|
|
|
++m_dependencies.rotation.revision;
|
|
|
|
|
m_operator.Prepare(state, m_dependencies, rotation);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-01 11:50:13 -04:00
|
|
|
[[nodiscard]] mfem::Vector residual() const {
|
|
|
|
|
mfem::Vector result;
|
|
|
|
|
m_operator.BuildResidual(result);
|
|
|
|
|
return result;
|
2026-08-30 16:41:14 -04:00
|
|
|
}
|
|
|
|
|
|
2026-09-01 11:50:13 -04:00
|
|
|
[[nodiscard]] mfem::Vector jacobian_action(const mfem::Vector &direction) const {
|
|
|
|
|
mfem::Vector result;
|
|
|
|
|
m_operator.Mult(direction, result);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
2026-08-30 16:41:14 -04:00
|
|
|
|
2026-09-01 11:50:13 -04:00
|
|
|
[[nodiscard]] mfem::Vector lifted_surface_direction(const mfem::Vector &rootDirection) const {
|
|
|
|
|
const auto &layout = m_operator.GetLayout();
|
|
|
|
|
const mfem::Vector surfaceDirection = const_value_view(rootDirection, layout, surfaceDeformationValue);
|
|
|
|
|
mfem::Vector volumeDirection(m_operator.GetDomainDeformation().volumeDisplacementSize());
|
|
|
|
|
m_operator.GetDomainDeformation().applyJacobian(
|
|
|
|
|
m_operator.GetSurfaceDeformationParameters(), surfaceDirection, volumeDirection
|
2026-08-30 16:41:14 -04:00
|
|
|
);
|
2026-09-01 11:50:13 -04:00
|
|
|
return volumeDirection;
|
2026-08-30 16:41:14 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
void initialize_state() {
|
|
|
|
|
report_progress(m_fem.mesh->GetComm(), "constructing the analytic n=3 Lane-Emden state");
|
|
|
|
|
|
|
|
|
|
constexpr double surfaceCoordinate = 6.8968486193769603755;
|
|
|
|
|
constexpr int radialSampleCount = 8192;
|
|
|
|
|
const double pi = std::acos(-1.0);
|
|
|
|
|
const double radius = mean_field::utils::RADIUS;
|
|
|
|
|
const double targetMass = mean_field::utils::MASS;
|
|
|
|
|
constexpr double dimensionlessMass = 2.0182359509662283534;
|
|
|
|
|
const double polytropicConstant =
|
|
|
|
|
pi * mean_field::utils::G * std::pow(targetMass / (4.0 * pi * dimensionlessMass), 2.0 / 3.0);
|
|
|
|
|
const double centralDensity =
|
|
|
|
|
std::pow(surfaceCoordinate * std::sqrt(polytropicConstant / (pi * mean_field::utils::G)) / radius, 3.0);
|
|
|
|
|
|
|
|
|
|
const mean_field::models::structure::StructureSeed seed =
|
|
|
|
|
m_model.makeInitialSeed({.centralDensity = centralDensity, .radialSampleCount = radialSampleCount});
|
|
|
|
|
|
|
|
|
|
const auto interpolate = [](const mfem::Vector &radii, const mfem::Vector &values, const double r) {
|
|
|
|
|
if (r <= radii(0)) {
|
|
|
|
|
return values(0);
|
|
|
|
|
}
|
|
|
|
|
const int finalIndex = radii.Size() - 1;
|
|
|
|
|
if (r >= radii(finalIndex)) {
|
|
|
|
|
return values(finalIndex);
|
|
|
|
|
}
|
|
|
|
|
int lower = 0;
|
|
|
|
|
int upper = finalIndex;
|
|
|
|
|
while (upper - lower > 1) {
|
|
|
|
|
const int middle = lower + (upper - lower) / 2;
|
|
|
|
|
if (radii(middle) <= r) {
|
|
|
|
|
lower = middle;
|
|
|
|
|
} else {
|
|
|
|
|
upper = middle;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const double fraction = (r - radii(lower)) / (radii(upper) - radii(lower));
|
|
|
|
|
return (1.0 - fraction) * values(lower) + fraction * values(upper);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mfem::FunctionCoefficient densityCoefficient([&seed, &interpolate](const mfem::Vector &position) {
|
|
|
|
|
const double r = position.Norml2();
|
|
|
|
|
return r >= seed.stellarRadius ? 0.0 : interpolate(seed.radius, seed.density, r);
|
|
|
|
|
});
|
|
|
|
|
mfem::FunctionCoefficient enthalpyCoefficient([&seed, &interpolate](const mfem::Vector &position) {
|
|
|
|
|
const double r = position.Norml2();
|
|
|
|
|
return r >= seed.stellarRadius ? 0.0 : interpolate(seed.radius, seed.enthalpy, r);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
mfem::ParGridFunction densityField(m_fem.densityFes.get());
|
|
|
|
|
mfem::ParGridFunction enthalpyField(m_fem.enthalpyFes.get());
|
|
|
|
|
mfem::ParGridFunction displacementField(m_fem.displacementFes.get());
|
|
|
|
|
densityField = 0.0;
|
|
|
|
|
enthalpyField = 0.0;
|
|
|
|
|
displacementField = 0.0;
|
|
|
|
|
densityField.ProjectCoefficient(densityCoefficient);
|
|
|
|
|
enthalpyField.ProjectCoefficient(enthalpyCoefficient);
|
|
|
|
|
*m_fem.displacement = displacementField;
|
|
|
|
|
|
|
|
|
|
report_progress(m_fem.mesh->GetComm(), "solving the gravity field for the seed state");
|
|
|
|
|
const mean_field::physics::GravitySolution gravity =
|
|
|
|
|
mean_field::physics::solve_gravity_field(m_fem, m_args, densityField, displacementField);
|
|
|
|
|
|
|
|
|
|
mfem::Vector densityTrue;
|
|
|
|
|
mfem::Vector enthalpyTrue;
|
|
|
|
|
mfem::Vector gravityGradientTrue;
|
|
|
|
|
mfem::Vector gravityPotentialTrue;
|
|
|
|
|
densityField.GetTrueDofs(densityTrue);
|
|
|
|
|
enthalpyField.GetTrueDofs(enthalpyTrue);
|
|
|
|
|
gravity.gradPhi.GetTrueDofs(gravityGradientTrue);
|
|
|
|
|
gravity.phi.GetTrueDofs(gravityPotentialTrue);
|
|
|
|
|
|
|
|
|
|
const auto &layout = m_operator.GetLayout();
|
|
|
|
|
const mean_field::field::FieldDofMap densityMap =
|
|
|
|
|
mean_field::field::make_field_dof_map<mean_field::field::Density, DomainSchema>(*m_fem.densityFes);
|
|
|
|
|
const mean_field::field::FieldDofMap enthalpyMap =
|
|
|
|
|
mean_field::field::make_field_dof_map<mean_field::field::Enthalpy, DomainSchema>(*m_fem.enthalpyFes);
|
|
|
|
|
|
2026-09-01 11:50:13 -04:00
|
|
|
mfem::Vector surfaceParameters(layout.size(surfaceDeformationValue));
|
|
|
|
|
surfaceParameters = 0.0;
|
2026-08-30 16:41:14 -04:00
|
|
|
assign_value_block(m_state, layout, densityValue, densityMap.gather(densityTrue));
|
2026-09-01 11:50:13 -04:00
|
|
|
assign_value_block(m_state, layout, surfaceDeformationValue, surfaceParameters);
|
2026-08-30 16:41:14 -04:00
|
|
|
assign_value_block(m_state, layout, gravityGradientValue, gravityGradientTrue);
|
|
|
|
|
assign_value_block(m_state, layout, gravityPotentialValue, gravityPotentialTrue);
|
|
|
|
|
assign_value_block(m_state, layout, enthalpyValue, enthalpyMap.gather(enthalpyTrue));
|
|
|
|
|
value_view(m_state, layout, bernoulliValue)(0) = -mean_field::utils::G * targetMass / radius;
|
|
|
|
|
|
|
|
|
|
m_currentState = m_state;
|
|
|
|
|
prepare(m_state, rotation(0.0));
|
|
|
|
|
report_progress(m_fem.mesh->GetComm(), "analytic state is prepared");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mean_field::utils::Args m_args;
|
|
|
|
|
mean_field::fem::FEM m_fem;
|
|
|
|
|
Model m_model;
|
|
|
|
|
mean_field::operators::PreparedStellarEquilibriumOperator m_operator;
|
|
|
|
|
mfem::Vector m_state;
|
|
|
|
|
mfem::Vector m_currentState;
|
|
|
|
|
mean_field::operators::StellarEquilibriumDependencies m_dependencies;
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-01 11:50:13 -04:00
|
|
|
enum class SurfaceModeKind : std::uint8_t {
|
|
|
|
|
uniform_radial,
|
|
|
|
|
translation_like_dipole,
|
|
|
|
|
oblate_quadrupole,
|
|
|
|
|
spherical_harmonic
|
|
|
|
|
};
|
2026-08-30 16:41:14 -04:00
|
|
|
|
2026-09-01 11:50:13 -04:00
|
|
|
struct SurfaceMode final {
|
2026-08-30 16:41:14 -04:00
|
|
|
std::string name;
|
2026-09-01 11:50:13 -04:00
|
|
|
SurfaceModeKind kind;
|
2026-08-30 16:41:14 -04:00
|
|
|
int axis;
|
|
|
|
|
mfem::Vector direction;
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-01 11:50:13 -04:00
|
|
|
[[nodiscard]] inline const char *surface_mode_kind_name(const SurfaceModeKind kind) noexcept {
|
|
|
|
|
switch (kind) {
|
|
|
|
|
case SurfaceModeKind::uniform_radial:
|
|
|
|
|
return "uniform_radial";
|
|
|
|
|
case SurfaceModeKind::translation_like_dipole:
|
|
|
|
|
return "translation_like_dipole";
|
|
|
|
|
case SurfaceModeKind::oblate_quadrupole:
|
|
|
|
|
return "oblate_quadrupole";
|
|
|
|
|
case SurfaceModeKind::spherical_harmonic:
|
|
|
|
|
return "spherical_harmonic";
|
|
|
|
|
}
|
|
|
|
|
return "unknown";
|
|
|
|
|
}
|
2026-08-30 16:41:14 -04:00
|
|
|
|
2026-09-01 11:50:13 -04:00
|
|
|
[[nodiscard]] inline double zonal_legendre(
|
|
|
|
|
const int degree,
|
|
|
|
|
const double cosineOfPolarAngle
|
|
|
|
|
) {
|
|
|
|
|
MFEM_VERIFY(degree >= 0, "A zonal spherical-harmonic degree must be non-negative.");
|
|
|
|
|
const double coordinate = std::clamp(cosineOfPolarAngle, -1.0, 1.0);
|
|
|
|
|
if (degree == 0) {
|
|
|
|
|
return 1.0;
|
|
|
|
|
}
|
|
|
|
|
if (degree == 1) {
|
|
|
|
|
return coordinate;
|
2026-08-30 16:41:14 -04:00
|
|
|
}
|
|
|
|
|
|
2026-09-01 11:50:13 -04:00
|
|
|
double previousPrevious = 1.0;
|
|
|
|
|
double previous = coordinate;
|
|
|
|
|
for (int order = 2; order <= degree; ++order) {
|
|
|
|
|
const double current = ((2.0 * static_cast<double>(order) - 1.0) * coordinate * previous -
|
|
|
|
|
(static_cast<double>(order) - 1.0) * previousPrevious) /
|
|
|
|
|
static_cast<double>(order);
|
|
|
|
|
previousPrevious = previous;
|
|
|
|
|
previous = current;
|
|
|
|
|
}
|
|
|
|
|
return previous;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[[nodiscard]] inline std::vector<SurfaceMode> make_surface_modes(N3Equilibrium &fixture) {
|
|
|
|
|
const auto &layout = fixture.stellar_operator().GetLayout();
|
|
|
|
|
auto deformation = fixture.model().compileDomainDeformation(fixture.fem());
|
|
|
|
|
const auto &surface = deformation.surfaceDeformationPrescription();
|
|
|
|
|
MFEM_VERIFY(
|
|
|
|
|
surface.parameterCount() == layout.size(surfaceDeformationValue),
|
|
|
|
|
"The diagnostic surface prescription does not match the root surface block."
|
|
|
|
|
);
|
2026-08-30 16:41:14 -04:00
|
|
|
|
2026-09-01 11:50:13 -04:00
|
|
|
const auto make_root_direction = [&layout](const mfem::Vector &surfaceDirection) {
|
2026-08-30 16:41:14 -04:00
|
|
|
mfem::Vector direction(layout.value_offsets().Last());
|
|
|
|
|
direction = 0.0;
|
2026-09-01 11:50:13 -04:00
|
|
|
assign_value_block(direction, layout, surfaceDeformationValue, surfaceDirection);
|
|
|
|
|
return direction;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
std::vector<SurfaceMode> modes;
|
|
|
|
|
modes.reserve(6);
|
|
|
|
|
|
|
|
|
|
mfem::Vector uniform(surface.parameterCount());
|
|
|
|
|
for (int parameter = 0; parameter < uniform.Size(); ++parameter) {
|
|
|
|
|
uniform(parameter) = surface.referenceRadius(parameter);
|
|
|
|
|
}
|
|
|
|
|
modes.push_back(
|
|
|
|
|
{.name = "uniform_radial_homology",
|
|
|
|
|
.kind = SurfaceModeKind::uniform_radial,
|
|
|
|
|
.axis = -1,
|
|
|
|
|
.direction = make_root_direction(uniform)}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
for (int axis = 0; axis < surface.spatialDimension(); ++axis) {
|
|
|
|
|
mfem::Vector dipole(surface.parameterCount());
|
|
|
|
|
for (int parameter = 0; parameter < dipole.Size(); ++parameter) {
|
|
|
|
|
dipole(parameter) = surface.radialDirection(parameter, axis);
|
|
|
|
|
}
|
|
|
|
|
modes.push_back(
|
|
|
|
|
{.name = std::string("translation_like_dipole_") + static_cast<char>('x' + axis),
|
|
|
|
|
.kind = SurfaceModeKind::translation_like_dipole,
|
|
|
|
|
.axis = axis,
|
|
|
|
|
.direction = make_root_direction(dipole)}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mfem::Vector quadrupole(surface.parameterCount());
|
|
|
|
|
for (int parameter = 0; parameter < quadrupole.Size(); ++parameter) {
|
|
|
|
|
const double polarDirection = surface.radialDirection(parameter, 2);
|
|
|
|
|
quadrupole(parameter) = surface.referenceRadius(parameter) * (1.0 - 3.0 * polarDirection * polarDirection);
|
2026-08-30 16:41:14 -04:00
|
|
|
}
|
2026-09-01 11:50:13 -04:00
|
|
|
modes.push_back(
|
|
|
|
|
{.name = "axisymmetric_oblate_quadrupole_z",
|
|
|
|
|
.kind = SurfaceModeKind::oblate_quadrupole,
|
|
|
|
|
.axis = 2,
|
|
|
|
|
.direction = make_root_direction(quadrupole)}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
constexpr int diagnosticAngularDegree = 12;
|
|
|
|
|
mfem::Vector sphericalHarmonic(surface.parameterCount());
|
|
|
|
|
double localMaximumMagnitude = 0.0;
|
|
|
|
|
for (int parameter = 0; parameter < sphericalHarmonic.Size(); ++parameter) {
|
|
|
|
|
const double angularValue = zonal_legendre(diagnosticAngularDegree, surface.radialDirection(parameter, 2));
|
|
|
|
|
sphericalHarmonic(parameter) = surface.referenceRadius(parameter) * angularValue;
|
|
|
|
|
localMaximumMagnitude = std::max(localMaximumMagnitude, std::abs(angularValue));
|
|
|
|
|
}
|
|
|
|
|
double globalMaximumMagnitude = 0.0;
|
|
|
|
|
MPI_Allreduce(
|
|
|
|
|
&localMaximumMagnitude, &globalMaximumMagnitude, 1, MPI_DOUBLE, MPI_MAX, fixture.fem().mesh->GetComm()
|
|
|
|
|
);
|
|
|
|
|
MFEM_VERIFY(globalMaximumMagnitude > 0.0, "The spherical-harmonic surface mode has zero amplitude.");
|
|
|
|
|
sphericalHarmonic /= globalMaximumMagnitude;
|
|
|
|
|
modes.push_back(
|
|
|
|
|
{.name = "zonal_spherical_harmonic_l12",
|
|
|
|
|
.kind = SurfaceModeKind::spherical_harmonic,
|
|
|
|
|
.axis = -1,
|
|
|
|
|
.direction = make_root_direction(sphericalHarmonic)}
|
|
|
|
|
);
|
|
|
|
|
|
2026-08-30 16:41:14 -04:00
|
|
|
return modes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[[nodiscard]] inline std::array<
|
|
|
|
|
double,
|
|
|
|
|
6>
|
|
|
|
|
residual_block_norms(
|
|
|
|
|
const mfem::Vector &action,
|
|
|
|
|
const mean_field::operators::StellarEquilibriumLayout &layout,
|
|
|
|
|
const MPI_Comm communicator
|
|
|
|
|
) {
|
|
|
|
|
return {
|
|
|
|
|
global_norm(const_residual_view(action, layout, gravityGradientResidual), communicator),
|
|
|
|
|
global_norm(const_residual_view(action, layout, gravityPotentialResidual), communicator),
|
|
|
|
|
global_norm(const_residual_view(action, layout, densityResidual), communicator),
|
2026-09-01 11:50:13 -04:00
|
|
|
global_norm(const_residual_view(action, layout, surfaceShapeResidual), communicator),
|
2026-08-30 16:41:14 -04:00
|
|
|
global_norm(const_residual_view(action, layout, enthalpyResidual), communicator),
|
|
|
|
|
global_norm(const_residual_view(action, layout, massResidual), communicator)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
} // namespace experiment::null_space
|