1936 lines
85 KiB
C++
1936 lines
85 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(
|
|
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, 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, 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);
|
|
}
|