428 lines
20 KiB
C++
428 lines
20 KiB
C++
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <optional>
|
|
#include <span>
|
|
#include <string_view>
|
|
#include <type_traits>
|
|
#include <utility>
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <mfem.hpp>
|
|
#include <mpi.h>
|
|
|
|
import mean_field;
|
|
import test_helpers;
|
|
|
|
namespace spherical_harmonic_equilibrium_test {
|
|
using namespace mean_field;
|
|
using Block = operators::RootBlockDescriptor;
|
|
|
|
[[nodiscard]] const Block &findBlock(
|
|
const std::span<const Block> blocks,
|
|
const std::string_view name
|
|
) {
|
|
const auto found =
|
|
std::find_if(blocks.begin(), blocks.end(), [name](const Block &block) { return block.stableId == name; });
|
|
REQUIRE(found != blocks.end());
|
|
return *found;
|
|
}
|
|
|
|
[[nodiscard]] mfem::Vector readBlock(
|
|
const mfem::Vector &vector,
|
|
const Block &block
|
|
) {
|
|
REQUIRE(block.offset >= 0);
|
|
REQUIRE(block.offset + block.size <= vector.Size());
|
|
mfem::Vector result(block.size);
|
|
for (int i = 0; i < block.size; ++i)
|
|
result(i) = vector(block.offset + i);
|
|
return result;
|
|
}
|
|
|
|
void writeBlock(
|
|
mfem::Vector &vector,
|
|
const Block &block,
|
|
const mfem::Vector &values
|
|
) {
|
|
REQUIRE(values.Size() == block.size);
|
|
for (int i = 0; i < block.size; ++i)
|
|
vector(block.offset + i) = values(i);
|
|
}
|
|
|
|
void checkClose(
|
|
const mfem::Vector &actual,
|
|
const mfem::Vector &expected,
|
|
const double relative = 2.0e-9,
|
|
const double absolute = 2.0e-12
|
|
) {
|
|
REQUIRE(actual.Size() == expected.Size());
|
|
mfem::Vector difference(actual);
|
|
difference -= expected;
|
|
const double scale = std::max(actual.Norml2(), expected.Norml2());
|
|
INFO("error = " << difference.Norml2() << ", scale = " << scale);
|
|
REQUIRE(std::isfinite(difference.Norml2()));
|
|
CHECK(difference.Norml2() <= absolute + relative * scale);
|
|
}
|
|
|
|
void checkBlocks(
|
|
const mfem::Vector &actual,
|
|
const mfem::Vector &expected,
|
|
const std::span<const Block> blocks,
|
|
const double relative = 2.0e-9,
|
|
const double absolute = 2.0e-12
|
|
) {
|
|
for (const Block &block : blocks) {
|
|
INFO("residual block = " << block.stableId);
|
|
checkClose(readBlock(actual, block), readBlock(expected, block), relative, absolute);
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] operators::StellarEquilibriumDependencies dependencies(const std::uint64_t revision) {
|
|
return {
|
|
.discretization = {.identity = 8101, .revision = 1},
|
|
.density = {.identity = 8103, .revision = revision},
|
|
.surfaceDeformation = {.identity = 8107, .revision = revision},
|
|
.gravityGradient = {.identity = 8111, .revision = revision},
|
|
.gravityPotential = {.identity = 8117, .revision = revision},
|
|
.enthalpy = {.identity = 8123, .revision = revision},
|
|
.bernoulliConstant = {.identity = 8129, .revision = revision},
|
|
.rotation = {.identity = 8131, .revision = revision},
|
|
.targetMass = {.identity = 8147, .revision = 1}
|
|
};
|
|
}
|
|
|
|
[[nodiscard]] physics::RigidRotation zeroRotation() {
|
|
mfem::Vector omega(3), center(3);
|
|
omega = 0.0;
|
|
center = 0.0;
|
|
return {omega, center};
|
|
}
|
|
|
|
template <bool GeneratedRotation> [[nodiscard]] auto makeModel() {
|
|
if constexpr (GeneratedRotation) {
|
|
return model::StellarModel(
|
|
eos::Polytrope({.n = 1.5, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
|
|
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}}),
|
|
integral::FixedAngularMomentum({.Jtotal = dimensions::AngularMomentumValue{0.01}})
|
|
);
|
|
} else {
|
|
return model::StellarModel(
|
|
eos::Polytrope({.n = 1.5, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
|
|
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}})
|
|
);
|
|
}
|
|
}
|
|
|
|
template <typename Problem>
|
|
void prepare(
|
|
Problem &problem,
|
|
const mfem::Vector &state,
|
|
const std::uint64_t revision
|
|
) {
|
|
if constexpr (Problem::hasFixedAngularMomentum) {
|
|
REQUIRE(problem.TryPrepare(state, dependencies(revision)).has_value());
|
|
} else {
|
|
REQUIRE(problem.TryPrepare(state, dependencies(revision), zeroRotation()).has_value());
|
|
}
|
|
}
|
|
|
|
// The oracle is the existing nodal problem, with explicit coefficient and
|
|
// dual-force maps. It never calls the native modal residual or Jacobian to
|
|
// construct an expected coupled result.
|
|
template <
|
|
typename ModalProblem,
|
|
typename NodalProblem>
|
|
[[nodiscard]] mfem::Vector liftState(
|
|
const ModalProblem &modal,
|
|
const NodalProblem &nodal,
|
|
const mfem::DenseMatrix &basis,
|
|
const mfem::Vector &source
|
|
) {
|
|
mfem::Vector result(nodal.StateSize());
|
|
result = 0.0;
|
|
for (const Block &destination : nodal.GetManifest().valueBlocks()) {
|
|
const Block &origin = findBlock(modal.GetManifest().valueBlocks(), destination.stableId);
|
|
const mfem::Vector values = readBlock(source, origin);
|
|
if (destination.stableId == "surface_deformation") {
|
|
REQUIRE(basis.Width() == origin.size);
|
|
REQUIRE(basis.Height() == destination.size);
|
|
mfem::Vector lifted(destination.size);
|
|
basis.Mult(values, lifted);
|
|
writeBlock(result, destination, lifted);
|
|
} else {
|
|
writeBlock(result, destination, values);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
template <
|
|
typename ModalProblem,
|
|
typename NodalProblem>
|
|
[[nodiscard]] mfem::Vector pullResidual(
|
|
const ModalProblem &modal,
|
|
const NodalProblem &nodal,
|
|
const mfem::DenseMatrix &basis,
|
|
const mfem::Vector &source
|
|
) {
|
|
mfem::Vector result(modal.EquationSize());
|
|
result = 0.0;
|
|
for (const Block &destination : modal.GetManifest().residualBlocks()) {
|
|
const Block &origin = findBlock(nodal.GetManifest().residualBlocks(), destination.stableId);
|
|
const mfem::Vector values = readBlock(source, origin);
|
|
if (destination.stableId == "surface_shape_balance") {
|
|
REQUIRE(basis.Height() == origin.size);
|
|
REQUIRE(basis.Width() == destination.size);
|
|
mfem::Vector pulled(destination.size);
|
|
basis.MultTranspose(values, pulled);
|
|
writeBlock(result, destination, pulled);
|
|
} else {
|
|
writeBlock(result, destination, values);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
template <bool GeneratedRotation> void checkCoupledEquivalence() {
|
|
const utils::Args args = test_utils::setup_args();
|
|
fem::FEM modalFem = fem::setup_fem(args.mesh_file, args, 0);
|
|
fem::FEM nodalFem = fem::setup_fem(args.mesh_file, args, 0);
|
|
REQUIRE(modalFem.okay());
|
|
REQUIRE(nodalFem.okay());
|
|
|
|
const deformation::SphericalHarmonicSurface prescription{{.maximumDegree = 4}};
|
|
using Schema = utils::domain::CoreEnvelopeVacuumDomainSchema;
|
|
const auto surfaceMap = field::make_stellar_surface_scalar_dof_map<Schema>(*modalFem.surfaceDeformationFes);
|
|
const auto preparedSurface = deformation::compileSurfaceDeformationPrescription(
|
|
prescription, deformation::SurfaceDeformationCompilationContext{*modalFem.surfaceDeformationFes, surfaceMap}
|
|
);
|
|
const mfem::DenseMatrix basis(preparedSurface.nodalBasis());
|
|
REQUIRE(basis.Width() == 25);
|
|
|
|
const normalization::PhysicalRieszDiagonal policy{dimensions::LengthValue{1.0}};
|
|
const auto stellarModel = makeModel<GeneratedRotation>();
|
|
auto modal = equilibrium::discretize(
|
|
stellarModel, equilibrium::makeStellarDiscretization(std::move(modalFem), policy, prescription)
|
|
);
|
|
auto nodal =
|
|
equilibrium::discretize(stellarModel, equilibrium::makeStellarDiscretization(std::move(nodalFem), policy));
|
|
using ModalProblem = std::remove_cvref_t<decltype(modal)>;
|
|
STATIC_CHECK_FALSE(ModalProblem::hasFixedCentralDensity);
|
|
STATIC_CHECK(ModalProblem::hasFixedAngularMomentum == GeneratedRotation);
|
|
STATIC_CHECK(ModalProblem::symbolicallySquare);
|
|
REQUIRE(modal.StateSize() == modal.EquationSize());
|
|
const Block &shape = findBlock(modal.GetManifest().valueBlocks(), "surface_deformation");
|
|
const Block &shapeRows = findBlock(modal.GetManifest().residualBlocks(), "surface_shape_balance");
|
|
REQUIRE(shape.size == 25);
|
|
REQUIRE(shapeRows.size == shape.size);
|
|
CHECK(nodal.StateSize() - modal.StateSize() == basis.Height() - basis.Width());
|
|
|
|
mfem::Vector state(modal.StateSize()), direction(modal.StateSize());
|
|
state = 0.0;
|
|
for (int i = 0; i < direction.Size(); ++i)
|
|
direction(i) = 0.02 * std::sin(0.31 * (i + 1));
|
|
const auto view = modal.GetManifest().stateView(state);
|
|
view.block(utils::blocks::density_field.mass_term) = 1.0;
|
|
view.block(utils::blocks::enthalpy_field.specific_term) = 0.7;
|
|
view.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term) = 0.8;
|
|
if constexpr (GeneratedRotation) {
|
|
view.block(utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term) = 0.04;
|
|
modal.GetManifest().stateView(direction).block(
|
|
utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term
|
|
) = 0.015;
|
|
}
|
|
// Nonzero orders on both real branches make an accidental m=0-only
|
|
// integration fail. Positive h keeps fractional powers differentiable;
|
|
// its nonzero boundary value is handled by the physical replacement rows.
|
|
for (int mode = 0; mode < shape.size; ++mode) {
|
|
state(shape.offset + mode) = 0.001 * std::sin(0.7 * (mode + 1));
|
|
direction(shape.offset + mode) = 0.002 * std::cos(0.41 * (mode + 1));
|
|
}
|
|
for (const std::string_view name : {"gravity_gradient", "gravity_potential"}) {
|
|
const Block &block = findBlock(modal.GetManifest().valueBlocks(), name);
|
|
for (int i = 0; i < block.size; ++i)
|
|
state(block.offset + i) = 0.01 * std::cos(0.13 * (i + 1));
|
|
}
|
|
|
|
const mfem::Vector nodalState = liftState(modal, nodal, basis, state);
|
|
const mfem::Vector nodalDirection = liftState(modal, nodal, basis, direction);
|
|
prepare(modal, state, 1);
|
|
prepare(nodal, nodalState, 1);
|
|
checkClose(
|
|
modal.GetPhysicalOperator().GetGeneratedVolumeDisplacement(),
|
|
nodal.GetPhysicalOperator().GetGeneratedVolumeDisplacement(), 2.0e-12, 2.0e-14
|
|
);
|
|
|
|
const auto &modalDomain = modal.GetPhysicalOperator().GetDomainDeformation();
|
|
const auto &nodalDomain = nodal.GetPhysicalOperator().GetDomainDeformation();
|
|
CHECK_FALSE(modalDomain.hasNodalRadialParameters());
|
|
CHECK(nodalDomain.hasNodalRadialParameters());
|
|
const Block &nodalShape = findBlock(nodal.GetManifest().valueBlocks(), "surface_deformation");
|
|
const mfem::Vector modalParameters = readBlock(state, shape);
|
|
const mfem::Vector nodalParameters = readBlock(nodalState, nodalShape);
|
|
mfem::Vector modalTrace(modalDomain.surfaceDisplacementSize());
|
|
mfem::Vector nodalTrace(nodalDomain.surfaceDisplacementSize());
|
|
modalDomain.applySurfaceJacobian(modalParameters, readBlock(direction, shape), modalTrace);
|
|
nodalDomain.applySurfaceJacobian(nodalParameters, readBlock(nodalDirection, nodalShape), nodalTrace);
|
|
checkClose(modalTrace, nodalTrace, 3.0e-13, 2.0e-14);
|
|
mfem::Vector traceDual(modalTrace.Size());
|
|
for (int i = 0; i < traceDual.Size(); ++i)
|
|
traceDual(i) = std::sin(0.23 * (i + 1));
|
|
mfem::Vector modalDual(shape.size), nodalDual(nodalShape.size), expectedDual(shape.size);
|
|
modalDomain.applySurfaceJacobianTranspose(modalParameters, traceDual, modalDual);
|
|
nodalDomain.applySurfaceJacobianTranspose(nodalParameters, traceDual, nodalDual);
|
|
basis.MultTranspose(nodalDual, expectedDual);
|
|
checkClose(modalDual, expectedDual, 3.0e-13, 2.0e-13);
|
|
|
|
mfem::Vector residual, nodalResidual, action, nodalAction;
|
|
modal.BuildResidual(residual);
|
|
nodal.BuildResidual(nodalResidual);
|
|
modal.ApplyLinearization(direction, action);
|
|
nodal.ApplyLinearization(nodalDirection, nodalAction);
|
|
checkBlocks(residual, pullResidual(modal, nodal, basis, nodalResidual), modal.GetManifest().residualBlocks());
|
|
checkBlocks(action, pullResidual(modal, nodal, basis, nodalAction), modal.GetManifest().residualBlocks());
|
|
|
|
// A surface-only perturbation must also reach the retained field rows;
|
|
// a full coupled comparison alone could overlook an inert shape block.
|
|
mfem::Vector shapeDirection(direction.Size());
|
|
shapeDirection = 0.0;
|
|
writeBlock(shapeDirection, shape, readBlock(direction, shape));
|
|
mfem::Vector shapeAction;
|
|
modal.ApplyLinearization(shapeDirection, shapeAction);
|
|
CHECK(
|
|
readBlock(shapeAction, findBlock(modal.GetManifest().residualBlocks(), "poisson_balance")).Norml2() >
|
|
1.0e-10
|
|
);
|
|
CHECK(readBlock(shapeAction, shapeRows).Norml2() > 1.0e-10);
|
|
|
|
constexpr double step = 2.0e-5;
|
|
mfem::Vector plus(state), minus(state), plusResidual, minusResidual;
|
|
plus.Add(step, direction);
|
|
minus.Add(-step, direction);
|
|
prepare(modal, plus, 2);
|
|
modal.BuildResidual(plusResidual);
|
|
prepare(modal, minus, 3);
|
|
modal.BuildResidual(minusResidual);
|
|
plusResidual -= minusResidual;
|
|
plusResidual /= 2.0 * step;
|
|
checkBlocks(plusResidual, action, modal.GetManifest().residualBlocks(), 2.0e-5, 2.0e-9);
|
|
|
|
auto normalized = normalization::makeNormalizedStellarEquilibriumOperator(modal);
|
|
const mfem::Vector fixedStateFactors(normalized.GetNormalization().StateFactors());
|
|
const mfem::Vector fixedResidualFactors(normalized.GetNormalization().ResidualFactors());
|
|
REQUIRE(fixedStateFactors.Size() == modal.StateSize());
|
|
REQUIRE(fixedResidualFactors.Size() == modal.EquationSize());
|
|
for (int i = 0; i < fixedStateFactors.Size(); ++i) {
|
|
REQUIRE(std::isfinite(fixedStateFactors(i)));
|
|
REQUIRE(fixedStateFactors(i) > 0.0);
|
|
}
|
|
for (int i = 0; i < fixedResidualFactors.Size(); ++i) {
|
|
REQUIRE(std::isfinite(fixedResidualFactors(i)));
|
|
REQUIRE(fixedResidualFactors(i) > 0.0);
|
|
}
|
|
const auto prepareNormalized = [&](const mfem::Vector &physical, const std::uint64_t revision) {
|
|
mfem::Vector coordinates;
|
|
normalized.NormalizeState(physical, coordinates);
|
|
if constexpr (GeneratedRotation) {
|
|
(void)normalized.Prepare(coordinates, dependencies(revision));
|
|
} else {
|
|
(void)normalized.Prepare(coordinates, dependencies(revision), zeroRotation());
|
|
}
|
|
REQUIRE(normalized.IsPrepared());
|
|
};
|
|
prepareNormalized(state, 4);
|
|
checkClose(normalized.GetPhysicalState(), state, 3.0e-14, 2.0e-14);
|
|
mfem::Vector normalizedDirection, normalizedAction, expectedAction;
|
|
normalized.NormalizeState(direction, normalizedDirection);
|
|
normalized.Mult(normalizedDirection, normalizedAction);
|
|
normalized.NormalizeResidual(action, expectedAction);
|
|
checkBlocks(normalizedAction, expectedAction, modal.GetManifest().residualBlocks(), 3.0e-10, 2.0e-12);
|
|
mfem::Vector normalizedResidual, expectedResidual;
|
|
normalized.BuildResidual(normalizedResidual);
|
|
normalized.NormalizeResidual(residual, expectedResidual);
|
|
checkBlocks(normalizedResidual, expectedResidual, modal.GetManifest().residualBlocks(), 3.0e-10, 2.0e-12);
|
|
|
|
// Repeated accepted-state-sized changes must not silently rotate or
|
|
// rescale persistent harmonic coordinates through a current-shape metric.
|
|
prepareNormalized(plus, 5);
|
|
prepareNormalized(state, 6);
|
|
for (int i = 0; i < fixedStateFactors.Size(); ++i)
|
|
CHECK(normalized.GetNormalization().StateFactors()(i) == fixedStateFactors(i));
|
|
for (int i = 0; i < fixedResidualFactors.Size(); ++i)
|
|
CHECK(normalized.GetNormalization().ResidualFactors()(i) == fixedResidualFactors(i));
|
|
mfem::Vector restoredResidual, restoredAction;
|
|
modal.BuildResidual(restoredResidual);
|
|
modal.ApplyLinearization(direction, restoredAction);
|
|
checkBlocks(restoredResidual, residual, modal.GetManifest().residualBlocks());
|
|
checkBlocks(restoredAction, action, modal.GetManifest().residualBlocks());
|
|
}
|
|
} // namespace spherical_harmonic_equilibrium_test
|
|
|
|
TEST_CASE(
|
|
"Native Harmonic Stellar Equilibrium Equals The Nodal Pullback Without A Central Constraint",
|
|
tags::stellar_equilibrium_problem_integration & tags::spherical_harmonic_surface
|
|
) {
|
|
int ranks = 0;
|
|
MPI_Comm_size(MPI_COMM_WORLD, &ranks);
|
|
if (ranks != 1) {
|
|
SUCCEED("The current radial domain extension and harmonic prescription require one MPI rank.");
|
|
return;
|
|
}
|
|
SECTION("Prescribed zero rotation") {
|
|
spherical_harmonic_equilibrium_test::checkCoupledEquivalence<false>();
|
|
}
|
|
SECTION("Generated rotation from nonzero target angular momentum") {
|
|
spherical_harmonic_equilibrium_test::checkCoupledEquivalence<true>();
|
|
}
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Harmonic Degree And Azimuthal Cutoffs Determine Native Coupled Block Sizes",
|
|
tags::stellar_equilibrium_problem_integration & tags::spherical_harmonic_surface
|
|
) {
|
|
using namespace mean_field;
|
|
using namespace spherical_harmonic_equilibrium_test;
|
|
int ranks = 0;
|
|
MPI_Comm_size(MPI_COMM_WORLD, &ranks);
|
|
if (ranks != 1) {
|
|
SUCCEED("The current radial domain extension and harmonic prescription require one MPI rank.");
|
|
return;
|
|
}
|
|
struct Case final {
|
|
int degree;
|
|
std::optional<int> order;
|
|
int count;
|
|
};
|
|
const std::array<Case, 3> cases{{{0, std::nullopt, 1}, {4, 0, 5}, {4, std::nullopt, 25}}};
|
|
for (const Case &test : cases) {
|
|
INFO("maximum degree = " << test.degree << ", expected parameters = " << test.count);
|
|
const utils::Args args = test_utils::setup_args();
|
|
fem::FEM finiteElements = fem::setup_fem(args.mesh_file, args, 0);
|
|
REQUIRE(finiteElements.okay());
|
|
auto problem = equilibrium::discretize(
|
|
makeModel<false>(),
|
|
equilibrium::makeStellarDiscretization(
|
|
std::move(finiteElements), normalization::PhysicalRieszDiagonal{dimensions::LengthValue{1.0}},
|
|
deformation::SphericalHarmonicSurface{
|
|
{.maximumDegree = test.degree, .maximumAzimuthalOrder = test.order}
|
|
}
|
|
)
|
|
);
|
|
CHECK(findBlock(problem.GetManifest().valueBlocks(), "surface_deformation").size == test.count);
|
|
CHECK(findBlock(problem.GetManifest().residualBlocks(), "surface_shape_balance").size == test.count);
|
|
CHECK(problem.StateSize() == problem.EquationSize());
|
|
auto normalized = normalization::makeNormalizedStellarEquilibriumOperator(problem);
|
|
REQUIRE(normalized.GetNormalization().StateSize() == problem.StateSize());
|
|
REQUIRE(normalized.GetNormalization().ResidualSize() == problem.EquationSize());
|
|
for (const mfem::Vector *factors :
|
|
{&normalized.GetNormalization().StateFactors(), &normalized.GetNormalization().ResidualFactors()}) {
|
|
for (int i = 0; i < factors->Size(); ++i) {
|
|
REQUIRE(std::isfinite((*factors)(i)));
|
|
REQUIRE((*factors)(i) > 0.0);
|
|
}
|
|
}
|
|
}
|
|
}
|