1818 lines
84 KiB
C++
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);
|
|
}
|