Files
MeanField/tests/preconditioning/specification_border.cpp

1818 lines
84 KiB
C++

#include <algorithm>
#include <cmath>
#include <concepts>
#include <cstdint>
#include <numbers>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
import mean_field;
import test_helpers;
namespace specification_border_test {
namespace blocks = mean_field::utils::blocks;
namespace preconditioning = mean_field::preconditioning;
class PhysicsFacingBorderConstraint;
class PreparedPhysicsFacingBorderConstraint;
struct PhysicsFacingBorderTerm final {
using value = blocks::generated_value_block<mean_field::models::BorderFor<PhysicsFacingBorderConstraint>>;
using residual =
blocks::generated_residual_block<mean_field::models::ResidualFor<PhysicsFacingBorderConstraint>>;
};
inline constexpr PhysicsFacingBorderTerm physicsFacingBorderTerm{};
class MockPhysicsBorderAction final {
public:
explicit MockPhysicsBorderAction(const PreparedPhysicsFacingBorderConstraint &prepared) noexcept;
template <
typename Direction,
typename Row>
[[nodiscard]] auto ApplyJacobianAction(
mean_field::stellar::Derivative<
mean_field::stellar::equation::OwnConstraint,
mean_field::stellar::state::SpecificEnthalpy>,
const Direction &direction,
Row &row
) const {
return row.add(m_phaseDerivative * direction.specificEnthalpy()(0));
}
template <
typename Direction,
typename Row>
[[nodiscard]] auto ApplyJacobianAction(
mean_field::stellar::Derivative<
mean_field::stellar::equation::HydrostaticBalance,
mean_field::stellar::state::OwnGeneratedCoordinate>,
const Direction &direction,
Row &row
) const {
return row.add(m_coefficient * direction.generatedCoordinate()(0));
}
private:
double m_coefficient;
double m_phaseDerivative;
};
class PhysicsFacingBorderConstraint final {
public:
struct Parameters final {
mean_field::dimensions::SpecificEnthalpyValue target;
};
using TargetValue = mean_field::dimensions::SpecificEnthalpyValue;
using ScalarDescription = mean_field::stellar::ScalarConstraint<
mean_field::dimensions::quantity::SpecificEnthalpy,
mean_field::dimensions::quantity::SpecificEnthalpy,
mean_field::dimensions::quantity::SpecificEnthalpy,
"test.phase.border",
"lambda_test",
"test.phase.residual",
"R_test">;
using ModelDefinition = mean_field::constraint::ScalarPhaseCondition<
PhysicsFacingBorderConstraint,
"PhysicsFacingBorderConstraint",
mean_field::stellar::Reads<mean_field::stellar::state::SpecificEnthalpy>,
mean_field::stellar::Changes<mean_field::stellar::equation::HydrostaticBalance>,
ScalarDescription>;
using SpecificationBorderPhysics = preconditioning::LocalSpecificationBorderPhysics<MockPhysicsBorderAction>;
using EquilibriumPhysics =
mean_field::operators::LocalSpecificationEquilibriumPhysics<PreparedPhysicsFacingBorderConstraint>;
explicit constexpr PhysicsFacingBorderConstraint(const Parameters parameters) noexcept
: m_target(parameters.target) {
}
[[nodiscard]] constexpr TargetValue target() const noexcept {
return m_target;
}
private:
TargetValue m_target;
};
struct PhysicsFacingPreparationReport final { };
class PreparedPhysicsFacingBorderConstraint final {
public:
using Report = PhysicsFacingPreparationReport;
explicit PreparedPhysicsFacingBorderConstraint(const PhysicsFacingBorderConstraint &specification) noexcept
: m_target(specification.target().value()) {
}
template <typename StateView> [[nodiscard]] Report PrepareAfterPhysical(const StateView &state) {
const auto enthalpy = state.specificEnthalpy();
const auto border = state.generatedCoordinate();
m_referenceEnthalpy = enthalpy(0);
m_border = border(0);
m_coefficient = coefficientFor(m_referenceEnthalpy);
m_phaseDerivative = m_coefficient + coefficientDerivative * (m_referenceEnthalpy - m_target);
m_phaseResidual = m_coefficient * (enthalpy(0) - m_target);
m_borderValue = m_coefficient * border(0);
m_isPrepared = true;
return {};
}
template <typename Row>
[[nodiscard]] auto AddResidual(
mean_field::stellar::equation::OwnConstraint,
Row &row
) const {
return row.add(m_phaseResidual);
}
template <typename Row>
[[nodiscard]] auto AddResidual(
mean_field::stellar::equation::HydrostaticBalance,
Row &row
) const {
return row.add(m_borderValue);
}
template <
typename Direction,
typename Row>
[[nodiscard]] auto AddJacobianAction(
mean_field::stellar::Derivative<
mean_field::stellar::equation::OwnConstraint,
mean_field::stellar::state::SpecificEnthalpy>,
const Direction &direction,
Row &row
) const {
return row.add(m_phaseDerivative * direction.specificEnthalpy()(0));
}
template <
typename Direction,
typename Row>
[[nodiscard]] auto AddJacobianAction(
mean_field::stellar::Derivative<
mean_field::stellar::equation::HydrostaticBalance,
mean_field::stellar::state::SpecificEnthalpy>,
const Direction &direction,
Row &row
) const {
return row.add(coefficientDerivative * m_border * direction.specificEnthalpy()(0));
}
template <
typename Direction,
typename Row>
[[nodiscard]] auto AddJacobianAction(
mean_field::stellar::Derivative<
mean_field::stellar::equation::HydrostaticBalance,
mean_field::stellar::state::OwnGeneratedCoordinate>,
const Direction &direction,
Row &row
) const {
return row.add(m_coefficient * direction.generatedCoordinate()(0));
}
[[nodiscard]] bool IsPrepared() const noexcept {
return m_isPrepared;
}
[[nodiscard]] double coefficient() const noexcept {
return m_coefficient;
}
[[nodiscard]] double phaseDerivative() const noexcept {
return m_phaseDerivative;
}
private:
[[nodiscard]] static constexpr double coefficientFor(const double referenceEnthalpy) noexcept {
return 1.0 + coefficientDerivative * referenceEnthalpy;
}
static constexpr double coefficientDerivative = 0.125;
double m_target{0.0};
double m_referenceEnthalpy{0.0};
double m_border{0.0};
double m_coefficient{1.0};
double m_phaseDerivative{1.0};
double m_phaseResidual{0.0};
double m_borderValue{0.0};
bool m_isPrepared{false};
};
inline MockPhysicsBorderAction::MockPhysicsBorderAction(
const PreparedPhysicsFacingBorderConstraint &prepared
) noexcept
: m_coefficient(prepared.coefficient()),
m_phaseDerivative(prepared.phaseDerivative()) {
}
/* This distinct problem type lets the test provide a trusted backend
* adapter in addition to the constraint's nested physics package without
* changing the ordinary PhysicsFacingProblem exercised below. */
using DualProviderModel = mean_field::model::StellarModel<mean_field::models::SpecificationSet<
mean_field::eos::Polytrope,
mean_field::surface::Isobaric,
mean_field::models::FixedTotalMass,
mean_field::models::FixedCentralDensity,
PhysicsFacingBorderConstraint>>;
using DualProviderProblem = mean_field::equilibrium::StellarEquilibriumProblem<DualProviderModel>;
} // namespace specification_border_test
/* Simulate a trusted library backend being added for a self-describing
* constraint that already supplies its astronomer-facing nested package. */
namespace mean_field::preconditioning::detail {
template <>
class PreparedSpecificationBorderAction<
specification_border_test::PhysicsFacingBorderConstraint,
specification_border_test::DualProviderProblem>
final {
public:
static constexpr bool registered = true;
explicit PreparedSpecificationBorderAction(const specification_border_test::DualProviderProblem &) noexcept {
}
void ApplyStructureToBorder(
const StellarStructureDirectionView &,
mfem::Vector &
) const noexcept {
}
void ApplyBorderToStructure(
const mfem::Vector &,
StellarStructureActionView
) const noexcept {
}
void ApplyBorderToBorder(
const mfem::Vector &,
mfem::Vector &
) const noexcept {
}
};
/* Adversarially claim that the built-in stellar-structure backend handles
* this extension's nonzero core-to-core edge. The Polytrope core does not
* authorize the specification, so the public topology audit must ignore
* this specialization and continue to reject the default structure PC. */
template <>
struct StellarStructureBackendHandledCouplings<
operators::PreparedStellarEquilibriumOperator,
specification_border_test::PhysicsFacingBorderConstraint> {
using Type = utils::blocks::type_list<operators::StellarEquilibriumJacobianCoupling<
utils::blocks::enthalpy::specific::residual,
utils::blocks::enthalpy::specific::value>>;
};
} // namespace mean_field::preconditioning::detail
namespace {
namespace backend = mean_field::preconditioning::backend;
namespace blocks = mean_field::utils::blocks;
namespace preconditioning = mean_field::preconditioning;
template <typename... Specifications>
using ModelWith = mean_field::model::StellarModel<mean_field::models::SpecificationSet<Specifications...>>;
using PhysicsFacingBorderConstraint = specification_border_test::PhysicsFacingBorderConstraint;
using MockPhysicsBorderAction = specification_border_test::MockPhysicsBorderAction;
class IncompletePhysicsFacingBorderConstraint final {
public:
struct Parameters final { };
using ModelDefinition = mean_field::constraint::PhaseCondition<
IncompletePhysicsFacingBorderConstraint,
"IncompletePhysicsFacingBorderConstraint",
mean_field::models::DependsOn<mean_field::models::stellar::state::SpecificEnthalpy>,
mean_field::models::Affects<mean_field::models::stellar::equation::HydrostaticBalance>>;
struct SpecificationBorderPhysics final {
static constexpr bool registered = true;
};
explicit constexpr IncompletePhysicsFacingBorderConstraint(Parameters) noexcept {
}
};
using BaseModel =
ModelWith<mean_field::eos::Polytrope, mean_field::surface::Isobaric, mean_field::models::FixedTotalMass>;
using CentralModel = ModelWith<
mean_field::eos::Polytrope,
mean_field::surface::Isobaric,
mean_field::models::FixedTotalMass,
mean_field::models::FixedCentralDensity>;
using AngularModel = ModelWith<
mean_field::eos::Polytrope,
mean_field::surface::Isobaric,
mean_field::models::FixedTotalMass,
mean_field::models::FixedAngularMomentum>;
using AngularCentralModel = mean_field::model::StellarModel<mean_field::models::SpecificationSet<
mean_field::eos::Polytrope,
mean_field::surface::Isobaric,
mean_field::models::FixedTotalMass,
mean_field::models::FixedAngularMomentum,
mean_field::models::FixedCentralDensity>>;
using PhysicsFacingModel = ModelWith<
mean_field::eos::Polytrope,
mean_field::surface::Isobaric,
mean_field::models::FixedTotalMass,
PhysicsFacingBorderConstraint>;
using ReorderedCentralModel = mean_field::model::StellarModel<mean_field::models::SpecificationSet<
mean_field::models::FixedCentralDensity,
mean_field::surface::Isobaric,
mean_field::models::FixedTotalMass,
mean_field::eos::Polytrope>>;
using BaseProblem = mean_field::equilibrium::StellarEquilibriumProblem<BaseModel>;
using CentralProblem = mean_field::equilibrium::StellarEquilibriumProblem<CentralModel>;
using AngularProblem = mean_field::equilibrium::StellarEquilibriumProblem<AngularModel>;
using AngularCentralProblem = mean_field::equilibrium::StellarEquilibriumProblem<AngularCentralModel>;
using PhysicsFacingProblem = mean_field::equilibrium::StellarEquilibriumProblem<PhysicsFacingModel>;
using DualProviderProblem = specification_border_test::DualProviderProblem;
using BaseBorder = preconditioning::CompiledSpecificationBorderFor<BaseModel>;
using CentralBorder = preconditioning::CompiledSpecificationBorderFor<CentralModel>;
using AngularBorder = preconditioning::CompiledSpecificationBorderFor<AngularModel>;
using AngularCentralBorder = preconditioning::CompiledSpecificationBorderFor<AngularCentralModel>;
using BaseComponent = decltype(preconditioning::specificationBorderBlock(std::declval<const BaseProblem &>()));
using BaseCouplingOperator = preconditioning::SpecificationBorderJacobianOperator<BaseProblem>;
using PreparedBaseBorder = preconditioning::PreparedSpecificationBorderBlock<BaseProblem, BaseComponent>;
using CentralComponent =
decltype(preconditioning::specificationBorderBlock(std::declval<const CentralProblem &>()));
using AngularComponent =
decltype(preconditioning::specificationBorderBlock(std::declval<const AngularProblem &>()));
using AngularCentralComponent =
decltype(preconditioning::specificationBorderBlock(std::declval<const AngularCentralProblem &>()));
using UnsupportedStructureComponent =
preconditioning::IdentityBlock<blocks::density::mass::value, blocks::density::mass::residual>;
using UnsupportedBorderComponent = preconditioning::SpecificationBorderBlock<
UnsupportedStructureComponent,
BaseModel,
typename BaseProblem::FormType,
typename BaseProblem::JacobianFormType>;
using BasePlan = preconditioning::PreconditionerPlan<BaseComponent>;
using CentralPlan = preconditioning::PreconditionerPlan<CentralComponent>;
using AngularPlan = preconditioning::PreconditionerPlan<AngularComponent>;
using AngularCentralPlan = preconditioning::PreconditionerPlan<AngularCentralComponent>;
using PhysicsFacingRieszDiscretization =
mean_field::equilibrium::StellarDiscretizationFor<mean_field::normalization::PhysicalRieszDiagonal<>>;
using PhysicsStructureToBorderAction =
preconditioning::SpecificationStructureToBorderActionView<PhysicsFacingBorderConstraint, PhysicsFacingProblem>;
using PhysicsBorderToStructureAction =
preconditioning::SpecificationBorderToStructureActionView<PhysicsFacingBorderConstraint, PhysicsFacingProblem>;
using PhysicsBorderToBorderAction =
preconditioning::SpecificationBorderToBorderActionView<PhysicsFacingBorderConstraint, PhysicsFacingProblem>;
using MassStructureToBorderAction =
preconditioning::SpecificationStructureToBorderActionView<mean_field::models::FixedTotalMass, BaseProblem>;
template <typename View>
concept HasDensityBlock = requires(const View &view) { view.density(); };
template <typename View>
concept HasSpecificEnthalpyBlock = requires(const View &view) { view.specificEnthalpy(); };
template <typename View>
concept HasGeneratedCoordinateBlock = requires(const View &view) { view.generatedCoordinate(); };
template <typename View>
concept HasConstraintResidualBlock = requires(const View &view) { view.constraintResidual(); };
struct AddBoundContribution final {
template <
typename Direction,
typename Row>
requires requires(
const Direction &direction,
Row &row
) {
direction.size();
row.add(1.0);
}
[[nodiscard]] auto operator()(
const Direction &,
Row &row
) const {
return row.add(1.0);
}
};
struct AddBoundVectorContribution final {
template <
typename Direction,
typename Row>
requires requires(
const Direction &direction,
Row &row,
const mfem::Vector &contribution
) {
direction.values();
row.add(contribution);
}
[[nodiscard]] auto operator()(
const Direction &direction,
Row &row
) const {
mfem::Vector contribution(direction.Size());
contribution = 0.0;
return row.add(contribution);
}
};
struct ReadSpecificEnthalpyAndAdd final {
template <
typename Direction,
typename Row>
requires requires(
const Direction &direction,
Row &row
) {
direction.specificEnthalpy();
row.add(direction(0));
}
[[nodiscard]] auto operator()(
const Direction &direction,
Row &row
) const {
return row.add(direction(0));
}
};
struct ReadDensityAndAdd final {
template <
typename Direction,
typename Row>
requires requires(
const Direction &direction,
Row &row
) {
direction.density();
row.add(direction(0));
}
[[nodiscard]] auto operator()(
const Direction &direction,
Row &row
) const {
return row.add(direction(0));
}
};
struct ReadSurfaceShapeAndAdd final {
template <
typename Direction,
typename Row>
requires requires(
const Direction &direction,
Row &row
) {
direction.surfaceShape();
row.add(direction(0));
}
[[nodiscard]] auto operator()(
const Direction &direction,
Row &row
) const {
return row.add(direction(0));
}
};
struct ReadGeneratedCoordinateAndAdd final {
template <
typename Direction,
typename Row>
requires requires(
const Direction &direction,
Row &row
) {
direction.generatedCoordinate();
row.add(direction(0));
}
[[nodiscard]] auto operator()(
const Direction &direction,
Row &row
) const {
return row.add(direction(0));
}
};
struct MutateSpecificEnthalpyAndAdd final {
template <
typename Direction,
typename Row>
requires requires(
Direction &direction,
Row &row
) {
direction.specificEnthalpy()(0) = 1.0;
row.add(1.0);
}
[[nodiscard]] auto operator()(
Direction &,
Row &row
) const {
return row.add(1.0);
}
};
struct AccessNamedHydrostaticRow final {
template <
typename Direction,
typename Row>
requires requires(
const Direction &,
const Row &row
) { row.specificEnthalpy(); }
void operator()(
const Direction &,
const Row &
) const noexcept {
}
};
template <typename View>
concept CanAddSpecificEnthalpyFromGenerated = requires(const View &view) {
view.addSpecificEnthalpyFrom(
specification_border_test::physicsFacingBorderTerm, ReadGeneratedCoordinateAndAdd{}
);
};
template <typename View>
concept CanAddConstraintResidualFromEnthalpy = requires(const View &view) {
view.addConstraintResidualFrom(blocks::enthalpy_field.specific_term, ReadSpecificEnthalpyAndAdd{});
};
template <typename View>
concept CanAddConstraintResidualFromGenerated = requires(const View &view) {
view.addConstraintResidualFrom(
specification_border_test::physicsFacingBorderTerm, ReadGeneratedCoordinateAndAdd{}
);
};
template <typename View>
concept CanClaimEnthalpyButReadDensity = requires(const View &view) {
view.addConstraintResidualFrom(blocks::enthalpy_field.specific_term, ReadDensityAndAdd{});
};
template <typename View>
concept CanMutateBoundEnthalpySource = requires(const View &view) {
view.addConstraintResidualFrom(blocks::enthalpy_field.specific_term, MutateSpecificEnthalpyAndAdd{});
};
template <typename View>
concept CanAccessNamedRowInsideCallback = requires(const View &view) {
view.addConstraintResidualFrom(blocks::enthalpy_field.specific_term, AccessNamedHydrostaticRow{});
};
template <typename View>
concept CanAddMassResidualFromSurface = requires(const View &view) {
view.addConstraintResidualFrom(blocks::surface_deformation_field.parameters_term, ReadSurfaceShapeAndAdd{});
};
template <typename View>
concept CanAddVectorMassResidualFromSurface = requires(const View &view) {
view.addConstraintResidualFrom(blocks::surface_deformation_field.parameters_term, AddBoundVectorContribution{});
};
template <typename View>
concept CanClaimDensityButReadSurface = requires(const View &view) {
view.addConstraintResidualFrom(blocks::density_field.mass_term, ReadSurfaceShapeAndAdd{});
};
template <typename View>
concept ExposesUnrestrictedVector = requires(const View &view) { view.vector(); };
template <typename Problem, typename Block>
concept CanPrepareSpecificationBorder =
requires(const Problem &problem, Block block) { preconditioning::prepare(problem, std::move(block)); };
template <typename Problem, typename Block>
concept CanPrepareSpecificationBorderFromTemporary =
requires(Block block) { preconditioning::prepare(std::declval<Problem &&>(), std::move(block)); };
template <typename Problem>
concept CanMakeDefaultStellarPreconditioner =
requires(const Problem &problem) { preconditioning::makePreconditioner(problem); };
/* This deliberately implements the old unrestricted provider protocol.
* It would be structurally usable, but must not be accepted as a
* third-party escape hatch around the declared-coupling views. */
struct UnsafeRawBorderProvider final {
static constexpr bool registered = true;
template <mean_field::models::ModelSpecification, typename Problem> class Prepared final {
public:
static constexpr bool registered = true;
explicit Prepared(const Problem &) noexcept {
}
void ApplyStructureToBorder(
const preconditioning::StellarStructureDirectionView &,
mfem::Vector &
) const noexcept {
}
void ApplyBorderToStructure(
const mfem::Vector &,
preconditioning::StellarStructureActionView
) const noexcept {
}
void ApplyBorderToBorder(
const mfem::Vector &,
mfem::Vector &
) const noexcept {
}
};
template <
mean_field::models::ModelSpecification Specification,
typename Problem>
[[nodiscard]] static Prepared<
Specification,
std::remove_cvref_t<Problem>>
prepare(const Problem &problem) {
return Prepared<Specification, std::remove_cvref_t<Problem>>{problem};
}
};
template <typename Problem> class NonMovablePhysicsBorderAction final {
public:
explicit NonMovablePhysicsBorderAction(const Problem &) noexcept {
}
NonMovablePhysicsBorderAction(const NonMovablePhysicsBorderAction &) = delete;
NonMovablePhysicsBorderAction(NonMovablePhysicsBorderAction &&) = delete;
void ApplyStructureToBorder(
preconditioning::SpecificationStructureToBorderActionView<
PhysicsFacingBorderConstraint,
Problem>
) const noexcept {
}
void ApplyBorderToStructure(
preconditioning::SpecificationBorderToStructureActionView<
PhysicsFacingBorderConstraint,
Problem>
) const noexcept {
}
void ApplyBorderToBorder(
preconditioning::SpecificationBorderToBorderActionView<
PhysicsFacingBorderConstraint,
Problem>
) const noexcept {
}
};
/* Complete old-style action whose only defect is accepting the enclosing
* Problem. The physics-facing wrapper must reject it even though every
* numerical hook is otherwise valid. */
template <typename Problem> class ProblemOnlyPhysicsBorderAction final {
public:
explicit ProblemOnlyPhysicsBorderAction(const Problem &) noexcept {
}
void ApplyStructureToBorder(
preconditioning::SpecificationStructureToBorderActionView<
PhysicsFacingBorderConstraint,
Problem>
) const noexcept {
}
void ApplyBorderToStructure(
preconditioning::SpecificationBorderToStructureActionView<
PhysicsFacingBorderConstraint,
Problem>
) const noexcept {
}
void ApplyBorderToBorder(
preconditioning::SpecificationBorderToBorderActionView<
PhysicsFacingBorderConstraint,
Problem>
) const noexcept {
}
};
class CompleteMockBorderAction final {
public:
static constexpr bool registered = true;
explicit CompleteMockBorderAction(const BaseProblem &) noexcept {
}
void ApplyStructureToBorder(
const preconditioning::StellarStructureDirectionView &,
mfem::Vector &
) const noexcept {
}
void ApplyBorderToStructure(
const mfem::Vector &,
preconditioning::StellarStructureActionView
) const noexcept {
}
void ApplyBorderToBorder(
const mfem::Vector &,
mfem::Vector &
) const noexcept {
}
};
class RegisteredButIncompleteBorderAction final {
public:
static constexpr bool registered = true;
explicit RegisteredButIncompleteBorderAction(const BaseProblem &) noexcept {
}
};
template <typename Problem> class IncompleteMockPhysicsBorderAction final {
public:
explicit IncompleteMockPhysicsBorderAction(const Problem &) noexcept {
}
};
class KnownBorderCouplings final {
public:
explicit KnownBorderCouplings(const int borderSize)
: m_borderSize(borderSize),
m_structureToBorder(
borderSize,
StructureSize()
),
m_borderToStructure(
StructureSize(),
borderSize
),
m_borderDiagonal(borderSize) {
if (borderSize <= 0) {
throw std::invalid_argument("The known border must have positive size.");
}
for (int row = 0; row < borderSize; ++row) {
for (int column = 0; column < StructureSize(); ++column) {
m_structureToBorder(row, column) = 0.04 * static_cast<double>((row + 1) * (column + 2));
m_borderToStructure(column, row) = -0.03 * static_cast<double>((column + 1) * (row + 2));
}
for (int column = 0; column < borderSize; ++column) {
m_borderDiagonal(row, column) =
row == column ? 2.0 + static_cast<double>(row) : 0.01 * static_cast<double>(row + column + 1);
}
}
}
[[nodiscard]] static constexpr int StructureSize() noexcept {
return 3;
}
[[nodiscard]] int BorderSize() const noexcept {
return m_borderSize;
}
void ApplyStructureToBorder(
const mfem::Vector &direction,
mfem::Vector &action
) const {
m_structureToBorder.Mult(direction, action);
}
void ApplyBorderToStructure(
const mfem::Vector &direction,
mfem::Vector &action
) const {
m_borderToStructure.Mult(direction, action);
}
void ApplyBorderToBorder(
const mfem::Vector &direction,
mfem::Vector &action
) const {
m_borderDiagonal.Mult(direction, action);
}
void IncreaseBorderDiagonal(const double increment) {
for (int index = 0; index < m_borderSize; ++index) {
m_borderDiagonal(index, index) += increment;
}
}
[[nodiscard]] const mfem::DenseMatrix &StructureToBorder() const noexcept {
return m_structureToBorder;
}
[[nodiscard]] const mfem::DenseMatrix &BorderToStructure() const noexcept {
return m_borderToStructure;
}
[[nodiscard]] const mfem::DenseMatrix &BorderDiagonal() const noexcept {
return m_borderDiagonal;
}
private:
int m_borderSize;
mfem::DenseMatrix m_structureToBorder;
mfem::DenseMatrix m_borderToStructure;
mfem::DenseMatrix m_borderDiagonal;
};
[[nodiscard]] double relativeError(
const mfem::Vector &left,
const mfem::Vector &right
) {
mfem::Vector difference(left);
difference -= right;
return difference.Norml2() / std::max({1.0, left.Norml2(), right.Norml2()});
}
template <
preconditioning::ApplicationContract StructureInverseContract =
preconditioning::ApplicationContract::stationary_linear>
void verifyKnownBorderFactorization(const int borderSize) {
mfem::Vector structureDiagonal(KnownBorderCouplings::StructureSize());
structureDiagonal(0) = 2.0;
structureDiagonal(1) = 3.0;
structureDiagonal(2) = 5.0;
auto structureInverse = backend::prepare(backend::Diagonal{}, structureDiagonal);
KnownBorderCouplings couplings(borderSize);
using Factorization =
preconditioning::SpecificationBorderFactorizationOperator<KnownBorderCouplings, StructureInverseContract>;
Factorization factorization(structureInverse, couplings);
constexpr bool cachesStructureResponse = Factorization::cachesStructureInverseBorderCoupling;
const auto expectedSchurEntry = [&](const int row, const int column) {
double correction = 0.0;
for (int inner = 0; inner < KnownBorderCouplings::StructureSize(); ++inner) {
correction += couplings.StructureToBorder()(row, inner) * couplings.BorderToStructure()(inner, column) /
structureDiagonal(inner);
}
return couplings.BorderDiagonal()(row, column) - correction;
};
for (int row = 0; row < borderSize; ++row) {
for (int column = 0; column < borderSize; ++column) {
CHECK(
factorization.GetSchurComplement()(row, column) ==
Catch::Approx(expectedSchurEntry(row, column)).margin(2.0e-14)
);
}
}
const int completeSize = KnownBorderCouplings::StructureSize() + borderSize;
mfem::DenseMatrix completeMatrix(completeSize);
completeMatrix = 0.0;
for (int index = 0; index < KnownBorderCouplings::StructureSize(); ++index) {
completeMatrix(index, index) = structureDiagonal(index);
}
for (int row = 0; row < KnownBorderCouplings::StructureSize(); ++row) {
for (int column = 0; column < borderSize; ++column) {
completeMatrix(row, KnownBorderCouplings::StructureSize() + column) =
couplings.BorderToStructure()(row, column);
completeMatrix(KnownBorderCouplings::StructureSize() + column, row) =
couplings.StructureToBorder()(column, row);
}
}
for (int row = 0; row < borderSize; ++row) {
for (int column = 0; column < borderSize; ++column) {
completeMatrix(
KnownBorderCouplings::StructureSize() + row, KnownBorderCouplings::StructureSize() + column
) = couplings.BorderDiagonal()(row, column);
}
}
mfem::Vector rightHandSide(completeSize);
for (int index = 0; index < completeSize; ++index) {
rightHandSide(index) = 0.25 + 0.17 * static_cast<double>(index + 1);
}
mfem::Vector actual(completeSize);
mfem::Vector expected(completeSize);
factorization.Mult(rightHandSide, actual);
mfem::DenseMatrixInverse exactInverse(completeMatrix);
exactInverse.Mult(rightHandSide, expected);
CHECK(relativeError(actual, expected) <= 2.0e-13);
const auto statisticsBeforeRefresh = factorization.GetStatistics();
CHECK(statisticsBeforeRefresh.setups == 1);
CHECK(statisticsBeforeRefresh.schurProbes == static_cast<std::uint64_t>(borderSize));
CHECK(statisticsBeforeRefresh.applications == 1);
CHECK(
statisticsBeforeRefresh.structureInverseApplications ==
static_cast<std::uint64_t>(borderSize + (cachesStructureResponse ? 1 : 2))
);
CHECK(
statisticsBeforeRefresh.cachedStructureInverseBorderApplications ==
static_cast<std::uint64_t>(cachesStructureResponse ? 1 : 0)
);
CHECK(statisticsBeforeRefresh.structureToBorderApplications == static_cast<std::uint64_t>(borderSize + 1));
CHECK(
statisticsBeforeRefresh.borderToStructureApplications ==
static_cast<std::uint64_t>(borderSize + (cachesStructureResponse ? 0 : 1))
);
CHECK(statisticsBeforeRefresh.borderToBorderApplications == static_cast<std::uint64_t>(borderSize));
CHECK(
structureInverse.GetStatistics().applications ==
static_cast<std::uint64_t>(borderSize + (cachesStructureResponse ? 1 : 2))
);
for (int index = 0; index < KnownBorderCouplings::StructureSize(); ++index) {
structureDiagonal(index) += 0.25 * static_cast<double>(index + 1);
completeMatrix(index, index) = structureDiagonal(index);
}
structureInverse.Refresh(structureDiagonal);
couplings.IncreaseBorderDiagonal(0.5);
for (int index = 0; index < borderSize; ++index) {
completeMatrix(
KnownBorderCouplings::StructureSize() + index, KnownBorderCouplings::StructureSize() + index
) += 0.5;
}
factorization.RefreshSchurComplement();
CHECK(factorization.GetStatistics().setups == 2);
CHECK(factorization.GetStatistics().schurProbes == static_cast<std::uint64_t>(2 * borderSize));
CHECK(factorization.GetStatistics().borderToBorderApplications == static_cast<std::uint64_t>(2 * borderSize));
CHECK(
factorization.GetStatistics().structureInverseApplications ==
static_cast<std::uint64_t>(2 * borderSize + (cachesStructureResponse ? 1 : 2))
);
CHECK(
factorization.GetStatistics().cachedStructureInverseBorderApplications ==
static_cast<std::uint64_t>(cachesStructureResponse ? 1 : 0)
);
CHECK(
factorization.GetStatistics().structureToBorderApplications ==
static_cast<std::uint64_t>(2 * borderSize + 1)
);
CHECK(
factorization.GetStatistics().borderToStructureApplications ==
static_cast<std::uint64_t>(2 * borderSize + (cachesStructureResponse ? 0 : 1))
);
for (int row = 0; row < borderSize; ++row) {
for (int column = 0; column < borderSize; ++column) {
CHECK(
factorization.GetSchurComplement()(row, column) ==
Catch::Approx(expectedSchurEntry(row, column)).margin(2.0e-14)
);
}
}
mfem::Vector refreshedActual(completeSize);
mfem::Vector refreshedExpected(completeSize);
factorization.Mult(rightHandSide, refreshedActual);
mfem::DenseMatrixInverse refreshedExactInverse(completeMatrix);
refreshedExactInverse.Mult(rightHandSide, refreshedExpected);
CHECK(relativeError(refreshedActual, refreshedExpected) <= 2.0e-13);
const auto statisticsAfterRefreshApplication = factorization.GetStatistics();
CHECK(statisticsAfterRefreshApplication.applications == 2);
CHECK(
statisticsAfterRefreshApplication.structureInverseApplications ==
static_cast<std::uint64_t>(2 * borderSize + (cachesStructureResponse ? 2 : 4))
);
CHECK(
statisticsAfterRefreshApplication.cachedStructureInverseBorderApplications ==
static_cast<std::uint64_t>(cachesStructureResponse ? 2 : 0)
);
CHECK(
statisticsAfterRefreshApplication.structureToBorderApplications ==
static_cast<std::uint64_t>(2 * borderSize + 2)
);
CHECK(
statisticsAfterRefreshApplication.borderToStructureApplications ==
static_cast<std::uint64_t>(2 * borderSize + (cachesStructureResponse ? 0 : 2))
);
}
[[nodiscard]] mean_field::operators::StellarEquilibriumDependencies
makeDependencies(const std::uint64_t revision = 1) {
return {
.discretization = {.identity = 9201, .revision = 1},
.density = {.identity = 9203, .revision = revision},
.surfaceDeformation = {.identity = 9207, .revision = revision},
.gravityGradient = {.identity = 9211, .revision = revision},
.gravityPotential = {.identity = 9217, .revision = revision},
.enthalpy = {.identity = 9223, .revision = revision},
.bernoulliConstant = {.identity = 9229, .revision = revision},
.rotation = {.identity = 9231, .revision = revision},
.targetMass = {.identity = 9237, .revision = 1}
};
}
[[nodiscard]] mean_field::physics::RigidRotation zeroRotation() {
mfem::Vector angularVelocity(3);
mfem::Vector center(3);
angularVelocity = 0.0;
center = 0.0;
return {angularVelocity, center};
}
template <
typename View,
typename Term>
void assignStateBlock(
const View &view,
const Term &term,
const mfem::Vector &source,
mfem::Vector &state
) {
mfem::Vector destination = view.block(term);
REQUIRE(destination.Size() == source.Size());
destination = source;
destination.SyncAliasMemory(state);
}
} // namespace
TEST_CASE(
"Model Specifications Compile Complete Canonical Preconditioning Borders",
"[preconditioning][specification_border][unit][type_contract]"
) {
using ExpectedBaseCorrections = blocks::type_list<blocks::fixed_total_mass::mass_normalization::value>;
using ExpectedBaseResiduals = blocks::type_list<blocks::fixed_total_mass::mass_normalization::residual>;
using ExpectedCentralCorrections = blocks::type_list<
blocks::fixed_total_mass::mass_normalization::value, blocks::fixed_central_density::central_value::value>;
using ExpectedCentralResiduals = blocks::type_list<
blocks::fixed_total_mass::mass_normalization::residual, blocks::fixed_central_density::central_value::residual>;
using ExpectedAngularCorrections = blocks::type_list<
blocks::fixed_total_mass::mass_normalization::value, blocks::fixed_angular_momentum::angular_velocity::value>;
using ExpectedAngularResiduals = blocks::type_list<
blocks::fixed_total_mass::mass_normalization::residual,
blocks::fixed_angular_momentum::angular_velocity::residual>;
using ExpectedAngularCentralCorrections = blocks::type_list<
blocks::fixed_total_mass::mass_normalization::value, blocks::fixed_angular_momentum::angular_velocity::value,
blocks::fixed_central_density::central_value::value>;
STATIC_CHECK(std::same_as<CentralModel, ReorderedCentralModel>);
STATIC_CHECK(BaseBorder::valueArity == 1);
STATIC_CHECK(BaseBorder::residualArity == 1);
STATIC_CHECK(BaseBorder::specificationCount == 1);
STATIC_CHECK(std::same_as<typename BaseBorder::CorrectionBlocks, ExpectedBaseCorrections>);
STATIC_CHECK(std::same_as<typename BaseBorder::ResidualBlocks, ExpectedBaseResiduals>);
STATIC_CHECK(BaseBorder::RequiredCouplings::size == 3);
STATIC_CHECK(CentralBorder::valueArity == 2);
STATIC_CHECK(CentralBorder::residualArity == 2);
STATIC_CHECK(CentralBorder::specificationCount == 2);
STATIC_CHECK(std::same_as<typename CentralBorder::CorrectionBlocks, ExpectedCentralCorrections>);
STATIC_CHECK(std::same_as<typename CentralBorder::ResidualBlocks, ExpectedCentralResiduals>);
STATIC_CHECK(CentralBorder::RequiredCouplings::size == 5);
STATIC_CHECK(AngularBorder::valueArity == 2);
STATIC_CHECK(AngularBorder::residualArity == 2);
STATIC_CHECK(AngularBorder::specificationCount == 2);
STATIC_CHECK(std::same_as<typename AngularBorder::CorrectionBlocks, ExpectedAngularCorrections>);
STATIC_CHECK(std::same_as<typename AngularBorder::ResidualBlocks, ExpectedAngularResiduals>);
STATIC_CHECK(AngularBorder::RequiredCouplings::size == 8);
STATIC_CHECK(AngularCentralBorder::valueArity == 3);
STATIC_CHECK(AngularCentralBorder::residualArity == 3);
STATIC_CHECK(AngularCentralBorder::specificationCount == 3);
STATIC_CHECK(std::same_as<typename AngularCentralBorder::CorrectionBlocks, ExpectedAngularCentralCorrections>);
STATIC_CHECK(AngularCentralBorder::RequiredCouplings::size == 10);
STATIC_CHECK(
preconditioning::specificationBorderValueOffset<mean_field::models::FixedTotalMass, CentralModel> == 0
);
STATIC_CHECK(
preconditioning::specificationBorderValueOffset<mean_field::models::FixedCentralDensity, CentralModel> == 1
);
STATIC_CHECK(
preconditioning::specificationBorderResidualOffset<mean_field::models::FixedTotalMass, CentralModel> == 0
);
STATIC_CHECK(
preconditioning::specificationBorderResidualOffset<mean_field::models::FixedCentralDensity, CentralModel> == 1
);
STATIC_CHECK(
preconditioning::specificationBorderValueOffset<mean_field::models::FixedTotalMass, AngularCentralModel> == 0
);
STATIC_CHECK(
preconditioning::specificationBorderValueOffset<
mean_field::models::FixedAngularMomentum, AngularCentralModel> == 1
);
STATIC_CHECK(
preconditioning::specificationBorderValueOffset<mean_field::models::FixedCentralDensity, AngularCentralModel> ==
2
);
STATIC_CHECK(preconditioning::PreconditionerComponent<BaseComponent>);
STATIC_CHECK(preconditioning::PreconditionerComponent<CentralComponent>);
STATIC_CHECK(preconditioning::PreconditionerComponent<AngularComponent>);
STATIC_CHECK(preconditioning::PreconditionerComponent<AngularCentralComponent>);
STATIC_CHECK(preconditioning::SpecificationBorderPreparableFor<BaseProblem, BaseComponent>);
STATIC_CHECK(preconditioning::SpecificationBorderPreparableFor<CentralProblem, CentralComponent>);
STATIC_CHECK(CanPrepareSpecificationBorder<BaseProblem, BaseComponent>);
STATIC_CHECK_FALSE(CanPrepareSpecificationBorderFromTemporary<BaseProblem, BaseComponent>);
STATIC_CHECK(std::constructible_from<BaseCouplingOperator, const BaseProblem &>);
STATIC_CHECK_FALSE(std::constructible_from<BaseCouplingOperator, BaseProblem &&>);
STATIC_CHECK_FALSE(std::constructible_from<BaseCouplingOperator, const BaseProblem &&>);
STATIC_CHECK(std::constructible_from<PreparedBaseBorder, const BaseProblem &, BaseComponent>);
STATIC_CHECK_FALSE(std::constructible_from<PreparedBaseBorder, BaseProblem &&, BaseComponent>);
STATIC_CHECK_FALSE(std::constructible_from<PreparedBaseBorder, const BaseProblem &&, BaseComponent>);
// A refreshed structure inverse invalidates the cached A^-1 B columns and
// the dense Schur complement even when the problem snapshot itself did not
// change. Keep the complete invalidation truth table executable at compile
// time so this lifecycle branch cannot silently regress.
STATIC_CHECK_FALSE(preconditioning::detail::specificationBorderCachesRequireRefresh(false, false));
STATIC_CHECK(preconditioning::detail::specificationBorderCachesRequireRefresh(true, false));
STATIC_CHECK(preconditioning::detail::specificationBorderCachesRequireRefresh(false, true));
STATIC_CHECK(preconditioning::detail::specificationBorderCachesRequireRefresh(true, true));
STATIC_CHECK(preconditioning::SpecificationBorderBlockType<UnsupportedBorderComponent>);
STATIC_CHECK_FALSE(preconditioning::SpecificationBorderPreparableFor<BaseProblem, UnsupportedBorderComponent>);
STATIC_CHECK_FALSE(CanPrepareSpecificationBorder<BaseProblem, UnsupportedBorderComponent>);
STATIC_CHECK(preconditioning::DefaultStellarPreconditionerAvailableFor<BaseProblem>);
STATIC_CHECK(preconditioning::DefaultStellarPreconditionerAvailableFor<CentralProblem>);
STATIC_CHECK(preconditioning::DefaultStellarPreconditionerAvailableFor<AngularProblem>);
STATIC_CHECK(preconditioning::DefaultStellarPreconditionerAvailableFor<AngularCentralProblem>);
STATIC_CHECK(CanMakeDefaultStellarPreconditioner<BaseProblem>);
STATIC_CHECK(CanMakeDefaultStellarPreconditioner<CentralProblem>);
STATIC_CHECK(CanMakeDefaultStellarPreconditioner<AngularProblem>);
STATIC_CHECK(CanMakeDefaultStellarPreconditioner<AngularCentralProblem>);
using PhysicsFacingStructureSupport =
preconditioning::DefaultStellarStructurePhysicalTopologySupport<PhysicsFacingModel>;
using UnhandledPhysicsFacingStructureEdge = mean_field::operators::StellarEquilibriumJacobianCoupling<
blocks::enthalpy::specific::residual, blocks::enthalpy::specific::value>;
STATIC_CHECK(PhysicsFacingStructureSupport::UnsupportedCouplings::size == 1);
STATIC_CHECK(
mean_field::utils::blocks::contains_type_v<
UnhandledPhysicsFacingStructureEdge, typename PhysicsFacingStructureSupport::UnsupportedCouplings>
);
STATIC_CHECK_FALSE(preconditioning::DefaultStellarPreconditionerAvailableFor<PhysicsFacingProblem>);
STATIC_CHECK_FALSE(CanMakeDefaultStellarPreconditioner<PhysicsFacingProblem>);
STATIC_CHECK_FALSE(
mean_field::operators::StellarEquilibriumRuntimeContribution<PhysicsFacingBorderConstraint>::registered
);
STATIC_CHECK_FALSE(
mean_field::operators::stellarEquilibriumBackendRuntimeAuthorized<
PhysicsFacingBorderConstraint, PhysicsFacingModel>
);
STATIC_CHECK(
mean_field::operators::StellarEquilibriumPhysicsAvailableFor<PhysicsFacingBorderConstraint, PhysicsFacingModel>
);
STATIC_CHECK(
mean_field::equilibrium::StellarEquilibriumModelDiscretizationCompatible<
PhysicsFacingModel, PhysicsFacingRieszDiscretization>
);
STATIC_CHECK(BaseComponent::RequiredCouplings::size == 20);
STATIC_CHECK(CentralComponent::RequiredCouplings::size == 22);
STATIC_CHECK(AngularComponent::RequiredCouplings::size == 25);
STATIC_CHECK(AngularCentralComponent::RequiredCouplings::size == 27);
STATIC_CHECK(preconditioning::CompletePreconditionerFor<BasePlan, typename BaseProblem::FormType>);
STATIC_CHECK(
preconditioning::CompatiblePreconditionerFor<
BasePlan, typename BaseProblem::FormType, typename BaseProblem::JacobianFormType>
);
STATIC_CHECK(preconditioning::CompletePreconditionerFor<CentralPlan, typename CentralProblem::FormType>);
STATIC_CHECK(
preconditioning::CompatiblePreconditionerFor<
CentralPlan, typename CentralProblem::FormType, typename CentralProblem::JacobianFormType>
);
STATIC_CHECK(preconditioning::CompletePreconditionerFor<AngularPlan, typename AngularProblem::FormType>);
STATIC_CHECK(
preconditioning::CompatiblePreconditionerFor<
AngularPlan, typename AngularProblem::FormType, typename AngularProblem::JacobianFormType>
);
STATIC_CHECK(
preconditioning::CompletePreconditionerFor<AngularCentralPlan, typename AngularCentralProblem::FormType>
);
STATIC_CHECK(
preconditioning::CompatiblePreconditionerFor<
AngularCentralPlan, typename AngularCentralProblem::FormType,
typename AngularCentralProblem::JacobianFormType>
);
STATIC_CHECK(preconditioning::backend::ArnoldiAdmissible<typename CentralComponent::BackendType>);
STATIC_CHECK(preconditioning::PreparedSpecificationBorderActionFor<CompleteMockBorderAction, BaseProblem>);
STATIC_CHECK_FALSE(
preconditioning::PreparedSpecificationBorderActionFor<RegisteredButIncompleteBorderAction, BaseProblem>
);
using MockPhysicsProvider =
preconditioning::LocalSpecificationBorderPhysics<specification_border_test::MockPhysicsBorderAction>;
using IncompleteMockPhysicsProvider =
preconditioning::SpecificationBorderPhysics<IncompleteMockPhysicsBorderAction>;
using NonMovableMockPhysicsProvider = preconditioning::SpecificationBorderPhysics<NonMovablePhysicsBorderAction>;
using ProblemOnlyMockPhysicsProvider = preconditioning::SpecificationBorderPhysics<ProblemOnlyPhysicsBorderAction>;
STATIC_CHECK(
preconditioning::SpecificationBorderPhysicsFor<
MockPhysicsProvider, PhysicsFacingBorderConstraint, PhysicsFacingProblem>
);
STATIC_CHECK(
std::constructible_from<
MockPhysicsBorderAction, const preconditioning::PreparedSpecificationEquilibriumPhysicsT<
PhysicsFacingBorderConstraint, PhysicsFacingProblem> &>
);
STATIC_CHECK_FALSE(std::constructible_from<MockPhysicsBorderAction, const PhysicsFacingProblem &>);
STATIC_CHECK_FALSE(
preconditioning::SpecificationBorderPhysicsFor<
IncompleteMockPhysicsProvider, PhysicsFacingBorderConstraint, PhysicsFacingProblem>
);
STATIC_CHECK_FALSE(preconditioning::DeclaredCouplingSafeSpecificationBorderPhysics<UnsafeRawBorderProvider>);
STATIC_CHECK_FALSE(
preconditioning::SpecificationBorderPhysicsFor<
UnsafeRawBorderProvider, PhysicsFacingBorderConstraint, PhysicsFacingProblem>
);
STATIC_CHECK_FALSE(
preconditioning::SpecificationBorderPhysicsFor<
NonMovableMockPhysicsProvider, PhysicsFacingBorderConstraint, PhysicsFacingProblem>
);
using PreparedPhysicsFacingEquilibrium =
preconditioning::PreparedSpecificationEquilibriumPhysicsT<PhysicsFacingBorderConstraint, PhysicsFacingProblem>;
using ProblemOnlyAction = ProblemOnlyPhysicsBorderAction<PhysicsFacingProblem>;
STATIC_CHECK(
std::same_as<PreparedPhysicsFacingEquilibrium, specification_border_test::PreparedPhysicsFacingBorderConstraint>
);
STATIC_CHECK(std::constructible_from<ProblemOnlyAction, const PhysicsFacingProblem &>);
STATIC_CHECK_FALSE(std::constructible_from<ProblemOnlyAction, const PreparedPhysicsFacingEquilibrium &>);
STATIC_CHECK_FALSE(
preconditioning::SpecificationBorderPhysicsFor<
ProblemOnlyMockPhysicsProvider, PhysicsFacingBorderConstraint, PhysicsFacingProblem>
);
STATIC_CHECK(
preconditioning::SpecificationBorderPhysicsAvailableFor<PhysicsFacingBorderConstraint, PhysicsFacingProblem>
);
STATIC_CHECK_FALSE(
preconditioning::SpecificationBorderPhysicsAvailableFor<PhysicsFacingBorderConstraint, BaseProblem>
);
STATIC_CHECK_FALSE(
preconditioning::SpecificationBorderPhysicsAvailableFor<IncompletePhysicsFacingBorderConstraint, BaseProblem>
);
STATIC_CHECK(
std::same_as<
preconditioning::PreparedSpecificationBorderPhysicsT<PhysicsFacingBorderConstraint, PhysicsFacingProblem>,
MockPhysicsProvider::Prepared<PhysicsFacingBorderConstraint, PhysicsFacingProblem>>
);
STATIC_CHECK(
std::same_as<
typename preconditioning::PreparedSpecificationBorderPhysicsT<
PhysicsFacingBorderConstraint, PhysicsFacingProblem>::Physics,
MockPhysicsBorderAction>
);
// An operation view exposes no direction at all until a declared exact
// Jacobian pair is selected. The callback then receives only that pair's
// source and an additive handle to only that pair's row.
STATIC_CHECK_FALSE(HasConstraintResidualBlock<PhysicsStructureToBorderAction>);
STATIC_CHECK(CanAddConstraintResidualFromEnthalpy<PhysicsStructureToBorderAction>);
STATIC_CHECK_FALSE(CanAddConstraintResidualFromGenerated<PhysicsStructureToBorderAction>);
STATIC_CHECK_FALSE(HasSpecificEnthalpyBlock<PhysicsStructureToBorderAction>);
STATIC_CHECK_FALSE(HasDensityBlock<PhysicsStructureToBorderAction>);
STATIC_CHECK_FALSE(CanClaimEnthalpyButReadDensity<PhysicsStructureToBorderAction>);
STATIC_CHECK_FALSE(CanMutateBoundEnthalpySource<PhysicsStructureToBorderAction>);
STATIC_CHECK_FALSE(CanAccessNamedRowInsideCallback<PhysicsStructureToBorderAction>);
// FixedTotalMass has both density and shape as legal structure sources.
// Even in that multi-source operation, selecting density cannot deliver
// the independently legal shape direction to the callback.
STATIC_CHECK(CanAddMassResidualFromSurface<MassStructureToBorderAction>);
STATIC_CHECK(CanAddVectorMassResidualFromSurface<MassStructureToBorderAction>);
STATIC_CHECK_FALSE(CanClaimDensityButReadSurface<MassStructureToBorderAction>);
STATIC_CHECK_FALSE(HasSpecificEnthalpyBlock<PhysicsBorderToStructureAction>);
STATIC_CHECK(CanAddSpecificEnthalpyFromGenerated<PhysicsBorderToStructureAction>);
STATIC_CHECK_FALSE(HasDensityBlock<PhysicsBorderToStructureAction>);
STATIC_CHECK_FALSE(HasConstraintResidualBlock<PhysicsBorderToBorderAction>);
STATIC_CHECK_FALSE(CanAddConstraintResidualFromGenerated<PhysicsBorderToBorderAction>);
STATIC_CHECK_FALSE(CanAddConstraintResidualFromEnthalpy<PhysicsBorderToBorderAction>);
STATIC_CHECK_FALSE(ExposesUnrestrictedVector<PhysicsStructureToBorderAction>);
}
TEST_CASE(
"A Nested And Backend Border Physics Provider Is Rejected As Ambiguous",
"[preconditioning][specification_border][physics-extension][type_contract]"
) {
using Specification = PhysicsFacingBorderConstraint;
using Problem = DualProviderProblem;
using NestedProvider = typename Specification::SpecificationBorderPhysics;
using BackendAction = preconditioning::detail::PreparedSpecificationBorderAction<Specification, Problem>;
using BackendProvider = preconditioning::detail::BuiltinSpecificationBorderPhysics<Specification>;
using Selection = preconditioning::detail::SpecificationBorderPhysicsSelectionAudit<Specification, Problem>;
using NestedOnlySelection = preconditioning::detail::SpecificationBorderPhysicsSelectionAudit<
PhysicsFacingBorderConstraint, PhysicsFacingProblem>;
using BackendOnlySelection = preconditioning::detail::SpecificationBorderPhysicsSelectionAudit<
mean_field::models::FixedTotalMass, BaseProblem>;
using MalformedProvider = preconditioning::SpecificationBorderPhysics<IncompleteMockPhysicsBorderAction>;
// Provider selection has four intentionally distinct outcomes. In
// particular, the ambiguity fixture uses a different model type, so its
// backend registration cannot contaminate the ordinary nested-only path.
STATIC_CHECK_FALSE(NestedOnlySelection::ambiguous);
STATIC_CHECK(NestedOnlySelection::available);
STATIC_CHECK(
preconditioning::SpecificationBorderPhysicsAvailableFor<PhysicsFacingBorderConstraint, PhysicsFacingProblem>
);
STATIC_CHECK_FALSE(BackendOnlySelection::ambiguous);
STATIC_CHECK(BackendOnlySelection::available);
STATIC_CHECK(
preconditioning::SpecificationBorderPhysicsAvailableFor<mean_field::models::FixedTotalMass, BaseProblem>
);
STATIC_CHECK_FALSE(
preconditioning::SpecificationBorderPhysicsFor<
MalformedProvider, PhysicsFacingBorderConstraint, PhysicsFacingProblem>
);
// Both implementations are independently complete. The aggregate path
// must still reject the model instead of silently preferring the nested one.
STATIC_CHECK(preconditioning::PreparedSpecificationBorderActionFor<BackendAction, Problem>);
STATIC_CHECK(preconditioning::SpecificationBorderPhysicsFor<BackendProvider, Specification, Problem>);
STATIC_CHECK(preconditioning::SpecificationBorderPhysicsFor<NestedProvider, Specification, Problem>);
STATIC_CHECK(Selection::ambiguous);
STATIC_CHECK_FALSE(Selection::available);
STATIC_CHECK_FALSE(preconditioning::SpecificationBorderPhysicsAvailableFor<Specification, Problem>);
STATIC_CHECK_FALSE(preconditioning::CompleteSpecificationBorderActionsFor<Problem>);
STATIC_CHECK_FALSE(preconditioning::DefaultStellarPreconditionerAvailableFor<Problem>);
// The public query remains safe for unrelated types as well as ambiguous
// valid problem types.
STATIC_CHECK_FALSE(preconditioning::SpecificationBorderPhysicsAvailableFor<int, int>);
}
TEST_CASE(
"Physics-Facing Constraint Hooks Reproduce Their Declared Jacobian Edges",
"[preconditioning][specification_border][physics-extension][integration]"
) {
using namespace mean_field;
const utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
constexpr double referenceRadius = 2.0;
constexpr double gravitationalConstant = 3.0;
constexpr double targetMass = 1.0;
const auto stellarModel = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{targetMass}}),
PhysicsFacingBorderConstraint({.target = dimensions::SpecificEnthalpyValue{1.0}})
);
auto problem = equilibrium::discretize(
stellarModel,
equilibrium::makeStellarDiscretization(
std::move(finiteElements),
normalization::PhysicalRieszDiagonal{dimensions::LengthValue{referenceRadius}, gravitationalConstant}
)
);
auto normalized = normalization::makeNormalizedStellarEquilibriumOperator(problem);
using Problem = std::remove_cvref_t<decltype(problem)>;
using Form = typename Problem::FormType;
constexpr auto customValueBlock =
utils::blocks::get_value_block<Form>(specification_border_test::physicsFacingBorderTerm);
constexpr auto customResidualBlock =
utils::blocks::get_residual_block<Form>(specification_border_test::physicsFacingBorderTerm);
const auto scales = normalization::deriveStellarCharacteristicScales(
dimensions::MassValue{targetMass}, dimensions::LengthValue{referenceRadius}, gravitationalConstant
);
const auto &layout = problem.GetManifest().layout();
CHECK(
normalized.GetNormalization().StateFactors()(layout.offset(customValueBlock)) ==
Catch::Approx(1.0 / scales.specificEnergy).epsilon(2.0e-15)
);
CHECK(
normalized.GetNormalization().ResidualFactors()(layout.offset(customResidualBlock)) ==
Catch::Approx(1.0 / scales.specificEnergy).epsilon(2.0e-15)
);
mfem::Vector state(problem.StateSize());
state = 0.0;
const auto stateView = problem.GetManifest().stateView(state);
stateView.block(blocks::density_field.mass_term) = 1.0;
stateView.block(blocks::enthalpy_field.specific_term) = 1.0;
stateView.block(blocks::fixed_total_mass_constraint.mass_normalization_term) = 0.25;
stateView.block(specification_border_test::physicsFacingBorderTerm) = 0.3;
const auto initialDependencies = makeDependencies(1);
problem.Prepare(state, initialDependencies, zeroRotation());
/* Independently differentiate the public residual. These two directions
* isolate the two custom edges, so agreement cannot be manufactured by
* comparing two copies of the mock's analytic formula. */
const auto centeredDifference = [&](const mfem::Vector &direction) {
constexpr double step = 1.0e-6;
mfem::Vector plusState(state);
plusState.Add(step, direction);
problem.Prepare(plusState, initialDependencies, zeroRotation());
mfem::Vector plusResidual;
problem.BuildResidual(plusResidual);
mfem::Vector minusState(state);
minusState.Add(-step, direction);
problem.Prepare(minusState, initialDependencies, zeroRotation());
mfem::Vector minusResidual;
problem.BuildResidual(minusResidual);
plusResidual -= minusResidual;
plusResidual /= 2.0 * step;
problem.Prepare(state, initialDependencies, zeroRotation());
return plusResidual;
};
mfem::Vector enthalpyOnlyDirection(problem.StateSize());
enthalpyOnlyDirection = 0.0;
auto enthalpyOnlyView = problem.GetManifest().stateView(enthalpyOnlyDirection);
mfem::Vector enthalpyOnlyBlock = enthalpyOnlyView.block(blocks::enthalpy_field.specific_term);
enthalpyOnlyBlock(0) = 0.7;
enthalpyOnlyBlock.SyncAliasMemory(enthalpyOnlyDirection);
mfem::Vector enthalpyOnlyAction;
problem.ApplyLinearization(enthalpyOnlyDirection, enthalpyOnlyAction);
mfem::Vector enthalpyOnlyDifference = centeredDifference(enthalpyOnlyDirection);
const auto enthalpyOnlyAnalyticView = problem.GetManifest().residualView(enthalpyOnlyAction);
const auto enthalpyOnlyDifferenceView = problem.GetManifest().residualView(enthalpyOnlyDifference);
CHECK(
enthalpyOnlyAnalyticView.block(specification_border_test::physicsFacingBorderTerm)(0) ==
Catch::Approx(enthalpyOnlyDifferenceView.block(specification_border_test::physicsFacingBorderTerm)(0))
.margin(2.0e-10)
);
mfem::Vector borderOnlyDirection(problem.StateSize());
borderOnlyDirection = 0.0;
auto borderOnlyView = problem.GetManifest().stateView(borderOnlyDirection);
mfem::Vector borderOnlyBlock = borderOnlyView.block(specification_border_test::physicsFacingBorderTerm);
borderOnlyBlock(0) = 0.4;
borderOnlyBlock.SyncAliasMemory(borderOnlyDirection);
mfem::Vector borderOnlyAction;
problem.ApplyLinearization(borderOnlyDirection, borderOnlyAction);
mfem::Vector borderOnlyDifference = centeredDifference(borderOnlyDirection);
const mfem::Vector analyticHydrostatic =
problem.GetManifest().residualView(borderOnlyAction).block(blocks::enthalpy_field.specific_term);
const mfem::Vector differenceHydrostatic =
problem.GetManifest().residualView(borderOnlyDifference).block(blocks::enthalpy_field.specific_term);
CHECK(relativeError(analyticHydrostatic, differenceHydrostatic) <= 2.0e-10);
preconditioning::SpecificationBorderJacobianOperator coupling(problem);
REQUIRE(coupling.BorderSize() == 2);
const auto &offsets = coupling.GetStructureOffsets();
const int enthalpySize = offsets[3] - offsets[2];
REQUIRE(enthalpySize > 0);
const mfem::Array<int> &surfaceRows = problem.GetPressureSurfaceRows().reduced_dofs();
REQUIRE(surfaceRows.Size() > 0);
const auto setExpectedHydrostaticBorderAction = [&offsets, &surfaceRows,
enthalpySize](mfem::Vector &action, const double contribution) {
for (int index = offsets[2]; index < offsets[3]; ++index) {
action(index) = contribution;
}
for (const int row : surfaceRows) {
REQUIRE(row >= 0);
REQUIRE(row < enthalpySize);
action(offsets[2] + row) = 0.0;
}
};
constexpr double enthalpyVariation = 0.7;
constexpr double borderVariation = 0.4;
mfem::Vector groupedDirection(coupling.Width());
groupedDirection = 0.0;
groupedDirection(offsets[2]) = enthalpyVariation;
groupedDirection(coupling.StructureSize() + 1) = borderVariation;
mfem::Vector actual(coupling.Height());
coupling.Mult(groupedDirection, actual);
mfem::Vector expected(coupling.Height());
expected = 0.0;
constexpr double initialCoefficient = 1.0 + 0.125 * 1.0;
setExpectedHydrostaticBorderAction(expected, initialCoefficient * borderVariation);
expected(coupling.StructureSize() + 1) = initialCoefficient * enthalpyVariation;
CHECK(relativeError(actual, expected) <= 2.0e-14);
// Compare each inferred cross block with the corresponding slice of the
// authoritative root Jacobian. Structure-to-structure physics is omitted
// deliberately; this operator owns only the specification border.
mfem::Vector structureRootDirection(problem.StateSize());
structureRootDirection = 0.0;
auto structureRootView = problem.GetManifest().stateView(structureRootDirection);
mfem::Vector enthalpyRootDirection = structureRootView.block(blocks::enthalpy_field.specific_term);
enthalpyRootDirection(0) = enthalpyVariation;
enthalpyRootDirection.SyncAliasMemory(structureRootDirection);
mfem::Vector borderRootDirection(problem.StateSize());
borderRootDirection = 0.0;
auto borderRootView = problem.GetManifest().stateView(borderRootDirection);
mfem::Vector customBorderDirection = borderRootView.block(specification_border_test::physicsFacingBorderTerm);
customBorderDirection(0) = borderVariation;
customBorderDirection.SyncAliasMemory(borderRootDirection);
mfem::Vector structureRootAction;
mfem::Vector borderRootAction;
problem.ApplyLinearization(structureRootDirection, structureRootAction);
problem.ApplyLinearization(borderRootDirection, borderRootAction);
const auto structureResidual = problem.GetManifest().residualView(structureRootAction);
const auto borderResidual = problem.GetManifest().residualView(borderRootAction);
CHECK(
structureResidual.block(specification_border_test::physicsFacingBorderTerm)(0) ==
Catch::Approx(actual(coupling.StructureSize() + 1)).margin(2.0e-14)
);
const mfem::Vector enthalpyBorderAction = borderResidual.block(blocks::enthalpy_field.specific_term);
const mfem::Vector expectedEnthalpyAction(actual.GetData() + offsets[2], enthalpySize);
CHECK(relativeError(enthalpyBorderAction, expectedEnthalpyAction) <= 2.0e-14);
/* A Newton-state relinearization invalidates the standalone inferred
* border operator even when dependency stamps are intentionally reused.
* This model is deliberately unavailable to the default full
* preconditioner because its additional nonzero h <- h term has no
* structure-backend implementation. */
mfem::Vector refreshedState(state);
auto refreshedStateView = problem.GetManifest().stateView(refreshedState);
mfem::Vector refreshedEnthalpy = refreshedStateView.block(blocks::enthalpy_field.specific_term);
refreshedEnthalpy = 3.0;
refreshedEnthalpy.SyncAliasMemory(refreshedState);
problem.Prepare(refreshedState, initialDependencies, zeroRotation());
CHECK_FALSE(coupling.IsCurrent());
CHECK_THROWS_AS(coupling.Mult(groupedDirection, actual), std::logic_error);
CHECK(coupling.Refresh());
CHECK(coupling.IsCurrent());
coupling.Mult(groupedDirection, actual);
constexpr double refreshedCoefficient = 1.0 + 0.125 * 3.0;
constexpr double refreshedPhaseDerivative = refreshedCoefficient + 0.125 * (3.0 - 1.0);
expected = 0.0;
setExpectedHydrostaticBorderAction(expected, refreshedCoefficient * borderVariation);
expected(coupling.StructureSize() + 1) = refreshedPhaseDerivative * enthalpyVariation;
CHECK(relativeError(actual, expected) <= 2.0e-14);
CHECK_FALSE(coupling.Refresh());
}
TEST_CASE(
"Dense Specification Borders Cache Stationary Structure Responses And Reproduce Exact Block Factorizations",
"[preconditioning][specification_border][unit][factorization]"
) {
SECTION("one generated scalar") {
verifyKnownBorderFactorization(1);
}
SECTION("two generated scalars") {
verifyKnownBorderFactorization(2);
}
SECTION("four generated scalars") {
verifyKnownBorderFactorization(4);
}
}
TEST_CASE(
"Flexible Specification Borders Preserve Per-Application Structure Solves",
"[preconditioning][specification_border][unit][factorization]"
) {
verifyKnownBorderFactorization<preconditioning::ApplicationContract::flexible>(2);
}
TEST_CASE(
"Generated Specification Border Actions Match The Authoritative Stellar Jacobian",
"[preconditioning][specification_border][integration]"
) {
using namespace mean_field;
const utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
constexpr double radius = utils::RADIUS;
constexpr double mass = utils::MASS;
const double polytropicConstant = 2.0 * utils::G * radius * radius / std::numbers::pi_v<double>;
const double centralDensity = std::numbers::pi_v<double> * mass / (4.0 * radius * radius * radius);
const auto stellarModel = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
);
auto problem = equilibrium::discretize(stellarModel, std::move(finiteElements));
auto projected = seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = 512}));
problem.Prepare(projected.values, makeDependencies(), zeroRotation());
preconditioning::SpecificationBorderJacobianOperator coupling(problem);
REQUIRE(coupling.BorderSize() == 2);
REQUIRE(coupling.StructureSize() + coupling.BorderSize() == problem.StateSize());
const auto &offsets = coupling.GetStructureOffsets();
using Form = typename std::remove_cvref_t<decltype(problem)>::FormType;
const auto &layout = problem.GetManifest().layout();
CHECK(offsets[1] - offsets[0] == layout.size(blocks::get_value_block<Form>(blocks::density_field.mass_term)));
CHECK(
offsets[2] - offsets[1] ==
layout.size(blocks::get_value_block<Form>(blocks::surface_deformation_field.parameters_term))
);
CHECK(offsets[3] - offsets[2] == layout.size(blocks::get_value_block<Form>(blocks::enthalpy_field.specific_term)));
CHECK(offsets[4] - offsets[3] == layout.size(blocks::get_value_block<Form>(blocks::gravity_field.gradient_term)));
CHECK(offsets[5] - offsets[4] == layout.size(blocks::get_value_block<Form>(blocks::gravity_field.poisson_term)));
mfem::Vector groupedDirection(coupling.Width());
for (int index = 0; index < groupedDirection.Size(); ++index) {
groupedDirection(index) = 0.015 * std::sin(0.23 * static_cast<double>(index + 1));
}
const auto groupedBlock = [&](const int block) {
return mfem::Vector(groupedDirection.GetData() + offsets[block], offsets[block + 1] - offsets[block]);
};
mfem::Vector structureOnlyRoot(problem.StateSize());
structureOnlyRoot = 0.0;
const auto structureView = problem.GetManifest().stateView(structureOnlyRoot);
assignStateBlock(structureView, blocks::density_field.mass_term, groupedBlock(0), structureOnlyRoot);
assignStateBlock(
structureView, blocks::surface_deformation_field.parameters_term, groupedBlock(1), structureOnlyRoot
);
assignStateBlock(structureView, blocks::enthalpy_field.specific_term, groupedBlock(2), structureOnlyRoot);
assignStateBlock(structureView, blocks::gravity_field.gradient_term, groupedBlock(3), structureOnlyRoot);
assignStateBlock(structureView, blocks::gravity_field.poisson_term, groupedBlock(4), structureOnlyRoot);
mfem::Vector borderOnlyRoot(problem.StateSize());
borderOnlyRoot = 0.0;
const auto borderView = problem.GetManifest().stateView(borderOnlyRoot);
mfem::Vector massDirection(groupedDirection.GetData() + coupling.StructureSize(), 1);
mfem::Vector centralDirection(groupedDirection.GetData() + coupling.StructureSize() + 1, 1);
assignStateBlock(
borderView, blocks::fixed_total_mass_constraint.mass_normalization_term, massDirection, borderOnlyRoot
);
assignStateBlock(
borderView, blocks::fixed_central_density_phase.central_value_term, centralDirection, borderOnlyRoot
);
mfem::Vector structureOnlyAction;
mfem::Vector borderOnlyAction;
problem.ApplyLinearization(structureOnlyRoot, structureOnlyAction);
problem.ApplyLinearization(borderOnlyRoot, borderOnlyAction);
auto structureOnlyResidual = problem.GetManifest().residualView(structureOnlyAction);
auto borderOnlyResidual = problem.GetManifest().residualView(borderOnlyAction);
mfem::Vector expected(coupling.Height());
expected = 0.0;
expected.SetVector(borderOnlyResidual.block(blocks::density_field.mass_term), offsets[0]);
expected.SetVector(borderOnlyResidual.block(blocks::surface_deformation_field.shape_equilibrium_term), offsets[1]);
expected.SetVector(borderOnlyResidual.block(blocks::enthalpy_field.specific_term), offsets[2]);
expected.SetVector(borderOnlyResidual.block(blocks::gravity_field.gradient_term), offsets[3]);
expected.SetVector(borderOnlyResidual.block(blocks::gravity_field.poisson_term), offsets[4]);
expected.SetVector(
structureOnlyResidual.block(blocks::fixed_total_mass_constraint.mass_normalization_term),
coupling.StructureSize()
);
expected.SetVector(
structureOnlyResidual.block(blocks::fixed_central_density_phase.central_value_term),
coupling.StructureSize() + 1
);
mfem::Vector borderDiagonal(2);
borderDiagonal(0) = borderOnlyResidual.block(blocks::fixed_total_mass_constraint.mass_normalization_term)(0);
borderDiagonal(1) = borderOnlyResidual.block(blocks::fixed_central_density_phase.central_value_term)(0);
mfem::Vector expectedBorder(expected, coupling.StructureSize(), coupling.BorderSize());
expectedBorder += borderDiagonal;
expectedBorder.SyncAliasMemory(expected);
mfem::Vector actual(coupling.Height());
coupling.Mult(groupedDirection, actual);
CHECK(relativeError(actual, expected) <= 2.0e-12);
auto component = preconditioning::makePreconditioner(problem);
using Component = decltype(component);
STATIC_CHECK(std::same_as<Component, CentralComponent>);
auto prepared = preconditioning::prepare(problem, component);
using GroupedPreconditioner = typename decltype(prepared)::GroupedPreconditioner;
using PreparedFactorization = typename GroupedPreconditioner::Factorization;
STATIC_CHECK(PreparedFactorization::cachesStructureInverseBorderCoupling);
mfem::Vector rightHandSide(prepared.Width());
for (int index = 0; index < rightHandSide.Size(); ++index) {
rightHandSide(index) = std::cos(0.11 * static_cast<double>(index + 1));
}
mfem::Vector correction(prepared.Height());
prepared.Mult(rightHandSide, correction);
for (int index = 0; index < correction.Size(); ++index) {
REQUIRE(std::isfinite(correction(index)));
}
const auto &factorizationStatistics = prepared.GetGroupedPreconditioner().GetFactorization().GetStatistics();
CHECK(factorizationStatistics.setups == 1);
CHECK(factorizationStatistics.schurProbes == 2);
CHECK(factorizationStatistics.applications == 1);
CHECK(factorizationStatistics.structureInverseApplications == 3);
CHECK(factorizationStatistics.cachedStructureInverseBorderApplications == 1);
CHECK(factorizationStatistics.borderToStructureApplications == 2);
const std::uint64_t setupsBeforeNoOpRefresh = factorizationStatistics.setups;
const auto unchanged = prepared.Refresh();
CHECK_FALSE(unchanged.DidAnyWork());
CHECK(prepared.GetGroupedPreconditioner().GetFactorization().GetStatistics().setups == setupsBeforeNoOpRefresh);
CHECK(prepared.IsCurrent());
/*
* A contribution may change with the prepared state even when callers
* intentionally reuse the same dependency stamps. The problem generation
* must therefore invalidate and rebuild the dense border Schur complement.
*/
problem.Prepare(projected.values, makeDependencies(), zeroRotation());
CHECK_FALSE(prepared.IsCurrent());
const std::uint64_t setupsBeforeRelinearization =
prepared.GetGroupedPreconditioner().GetFactorization().GetStatistics().setups;
const auto relinearized = prepared.Refresh();
CHECK(relinearized.rebuiltSchurComplement);
CHECK(relinearized.DidAnyWork());
CHECK(
prepared.GetGroupedPreconditioner().GetFactorization().GetStatistics().setups == setupsBeforeRelinearization + 1
);
CHECK(prepared.IsCurrent());
}
TEST_CASE(
"Fixed Angular Momentum Border Actions Match The Authoritative Generated Rotation Jacobian",
"[preconditioning][specification_border][fixed-angular-momentum][integration]"
) {
using namespace mean_field;
const utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
auto model = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}}),
integral::FixedAngularMomentum({.Jtotal = dimensions::AngularMomentumValue{0.2}})
);
auto problem = equilibrium::discretize(model, std::move(finiteElements));
mfem::Vector state(problem.StateSize());
state = 0.0;
const auto stateView = problem.GetManifest().stateView(state);
stateView.block(blocks::density_field.mass_term) = 1.0;
stateView.block(blocks::enthalpy_field.specific_term) = 1.0;
stateView.block(blocks::fixed_total_mass_constraint.mass_normalization_term) = 0.25;
stateView.block(blocks::fixed_angular_momentum_constraint.angular_velocity_term) = 0.4;
problem.Prepare(state, makeDependencies());
preconditioning::SpecificationBorderJacobianOperator coupling(problem);
REQUIRE(coupling.BorderSize() == 2);
REQUIRE(coupling.StructureSize() + coupling.BorderSize() == problem.StateSize());
const auto &offsets = coupling.GetStructureOffsets();
mfem::Vector groupedDirection(coupling.Width());
for (int index = 0; index < groupedDirection.Size(); ++index) {
groupedDirection(index) = 0.017 * std::sin(0.29 * static_cast<double>(index + 1));
}
const auto groupedBlock = [&](const int block) {
return mfem::Vector(groupedDirection.GetData() + offsets[block], offsets[block + 1] - offsets[block]);
};
mfem::Vector structureOnlyRoot(problem.StateSize());
structureOnlyRoot = 0.0;
const auto structureView = problem.GetManifest().stateView(structureOnlyRoot);
assignStateBlock(structureView, blocks::density_field.mass_term, groupedBlock(0), structureOnlyRoot);
assignStateBlock(
structureView, blocks::surface_deformation_field.parameters_term, groupedBlock(1), structureOnlyRoot
);
assignStateBlock(structureView, blocks::enthalpy_field.specific_term, groupedBlock(2), structureOnlyRoot);
assignStateBlock(structureView, blocks::gravity_field.gradient_term, groupedBlock(3), structureOnlyRoot);
assignStateBlock(structureView, blocks::gravity_field.poisson_term, groupedBlock(4), structureOnlyRoot);
mfem::Vector borderOnlyRoot(problem.StateSize());
borderOnlyRoot = 0.0;
const auto borderView = problem.GetManifest().stateView(borderOnlyRoot);
mfem::Vector massDirection(groupedDirection.GetData() + coupling.StructureSize(), 1);
mfem::Vector angularVelocityDirection(groupedDirection.GetData() + coupling.StructureSize() + 1, 1);
assignStateBlock(
borderView, blocks::fixed_total_mass_constraint.mass_normalization_term, massDirection, borderOnlyRoot
);
assignStateBlock(
borderView, blocks::fixed_angular_momentum_constraint.angular_velocity_term, angularVelocityDirection,
borderOnlyRoot
);
mfem::Vector structureOnlyAction;
mfem::Vector borderOnlyAction;
problem.ApplyLinearization(structureOnlyRoot, structureOnlyAction);
problem.ApplyLinearization(borderOnlyRoot, borderOnlyAction);
auto structureOnlyResidual = problem.GetManifest().residualView(structureOnlyAction);
auto borderOnlyResidual = problem.GetManifest().residualView(borderOnlyAction);
CHECK(borderOnlyResidual.block(blocks::surface_deformation_field.shape_equilibrium_term).Norml2() > 0.0);
CHECK(borderOnlyResidual.block(blocks::enthalpy_field.specific_term).Norml2() > 0.0);
CHECK(borderOnlyResidual.block(blocks::fixed_angular_momentum_constraint.angular_velocity_term)(0) != 0.0);
mfem::Vector expected(coupling.Height());
expected = 0.0;
expected.SetVector(borderOnlyResidual.block(blocks::density_field.mass_term), offsets[0]);
expected.SetVector(borderOnlyResidual.block(blocks::surface_deformation_field.shape_equilibrium_term), offsets[1]);
expected.SetVector(borderOnlyResidual.block(blocks::enthalpy_field.specific_term), offsets[2]);
expected.SetVector(borderOnlyResidual.block(blocks::gravity_field.gradient_term), offsets[3]);
expected.SetVector(borderOnlyResidual.block(blocks::gravity_field.poisson_term), offsets[4]);
expected.SetVector(
structureOnlyResidual.block(blocks::fixed_total_mass_constraint.mass_normalization_term),
coupling.StructureSize()
);
expected.SetVector(
structureOnlyResidual.block(blocks::fixed_angular_momentum_constraint.angular_velocity_term),
coupling.StructureSize() + 1
);
mfem::Vector expectedBorder(expected, coupling.StructureSize(), coupling.BorderSize());
expectedBorder(0) += borderOnlyResidual.block(blocks::fixed_total_mass_constraint.mass_normalization_term)(0);
expectedBorder(1) += borderOnlyResidual.block(blocks::fixed_angular_momentum_constraint.angular_velocity_term)(0);
expectedBorder.SyncAliasMemory(expected);
mfem::Vector actual(coupling.Height());
coupling.Mult(groupedDirection, actual);
CHECK(relativeError(actual, expected) <= 2.0e-12);
auto component = preconditioning::makePreconditioner(problem);
using Component = decltype(component);
STATIC_CHECK(std::same_as<Component, AngularComponent>);
auto prepared = preconditioning::prepare(problem, component);
CHECK(prepared.IsCurrent());
mfem::Vector rightHandSide(prepared.Width());
for (int index = 0; index < rightHandSide.Size(); ++index) {
rightHandSide(index) = std::cos(0.13 * static_cast<double>(index + 1));
}
mfem::Vector correction(prepared.Height());
prepared.Mult(rightHandSide, correction);
REQUIRE(correction.Size() == prepared.Height());
for (int index = 0; index < correction.Size(); ++index) {
CHECK(std::isfinite(correction(index)));
}
const auto &statistics = prepared.GetGroupedPreconditioner().GetFactorization().GetStatistics();
CHECK(statistics.setups == 1);
CHECK(statistics.schurProbes == 2);
}