feat(surface): major work on implementing surface constraints in a presciption agnostic manner

This commit is contained in:
2026-08-30 16:41:14 -04:00
parent 36adfa1174
commit 0a7f18c5c7
95 changed files with 30144 additions and 25766 deletions

View File

@@ -1,4 +1,6 @@
#include <array>
#include <cmath>
#include <concepts>
#include <limits>
#include <memory>
#include <type_traits>
@@ -12,17 +14,11 @@ import test_helpers;
namespace {
struct StellarModelExtensionTracker final {
int structureValidationCount{0};
int surfaceValidationCount{0};
int surfaceResolutionCount{0};
const mean_field::eos::EquationOfState *structureEquationOfState{nullptr};
const mean_field::eos::EquationOfState *surfaceValidationEquationOfState{nullptr};
const mean_field::eos::EquationOfState *surfaceResolutionEquationOfState{nullptr};
const mean_field::eos::Polytrope *structureEquationOfState{nullptr};
};
class StellarModelTestStructure final : public mean_field::models::structure::StructureBase {
class StellarModelTestStructure final {
public:
explicit StellarModelTestStructure(std::shared_ptr<StellarModelExtensionTracker> tracker)
: m_tracker(std::move(tracker)),
@@ -32,17 +28,17 @@ namespace {
) {
}
[[nodiscard]] const mean_field::eos::EquationOfState &equationOfState() const noexcept override {
[[nodiscard]] const mean_field::eos::Polytrope &equationOfState() const noexcept {
m_tracker->structureEquationOfState = &m_equationOfState;
return m_equationOfState;
}
[[nodiscard]] double targetMass() const noexcept override {
[[nodiscard]] double targetMass() const noexcept {
return 2.5;
}
[[nodiscard]] mean_field::models::structure::StructureSeed
makeInitialSeed(const mean_field::models::structure::StructureSeedRequest &request) const override {
makeInitialSeed(const mean_field::models::structure::StructureSeedRequest &request) const {
mean_field::models::structure::StructureSeed seed;
seed.radius.SetSize(2);
@@ -65,7 +61,7 @@ namespace {
return seed;
}
void validate() const override {
void validate() const {
++m_tracker->structureValidationCount;
}
@@ -74,59 +70,85 @@ namespace {
mean_field::eos::Polytrope m_equationOfState;
};
class StellarModelTestSurface final : public mean_field::surface::SurfaceBase {
class StructureWithoutSeed final {
public:
explicit StellarModelTestSurface(std::shared_ptr<StellarModelExtensionTracker> tracker)
: m_tracker(std::move(tracker)) {
}
[[nodiscard]] const mean_field::eos::Polytrope &equationOfState() const noexcept;
[[nodiscard]]
mean_field::surface::ResolvedSurfaceCondition
resolve(const mean_field::eos::EquationOfState &equationOfState) const override {
++m_tracker->surfaceResolutionCount;
[[nodiscard]] double targetMass() const noexcept;
m_tracker->surfaceResolutionEquationOfState = &equationOfState;
return mean_field::surface::ResolvedSurfaceCondition{0.375};
}
void validate(const mean_field::eos::EquationOfState &equationOfState) const override {
++m_tracker->surfaceValidationCount;
m_tracker->surfaceValidationEquationOfState = &equationOfState;
}
private:
std::shared_ptr<StellarModelExtensionTracker> m_tracker;
void validate() const;
};
class SurfaceWithoutPhysicalQuantity final { };
struct ModelSurfaceState final {
double specificEnthalpy;
[[nodiscard]] mean_field::eos::SpecificEnthalpyValue
value(mean_field::eos::quantity::SpecificEnthalpy) const noexcept {
return mean_field::eos::SpecificEnthalpyValue{specificEnthalpy};
}
};
using PolytropicStellarModel = mean_field::models::StellarModel<mean_field::models::structure::PolytropicStructure>;
using ExtensionStellarModel = mean_field::models::StellarModel<StellarModelTestStructure>;
} // namespace
TEST_CASE(
"Stellar Model Owns Structure And Surface Prescriptions",
tags::barotrope &tags::unit &tags::model
tags::stellar_model_type_contract
) {
STATIC_REQUIRE_FALSE(std::is_copy_constructible_v<mean_field::models::StellarModel>);
STATIC_CHECK(mean_field::models::StructurePrescription<mean_field::models::structure::PolytropicStructure>);
STATIC_CHECK(mean_field::models::StructurePrescription<StellarModelTestStructure>);
STATIC_CHECK_FALSE(mean_field::models::StructurePrescription<StructureWithoutSeed>);
STATIC_REQUIRE_FALSE(std::is_copy_assignable_v<mean_field::models::StellarModel>);
STATIC_CHECK(
mean_field::models::SurfacePrescription<
mean_field::surface::ConstantPressureSurface, mean_field::eos::Polytrope>
);
STATIC_CHECK_FALSE(
mean_field::models::SurfacePrescription<SurfaceWithoutPhysicalQuantity, mean_field::eos::Polytrope>
);
STATIC_REQUIRE(std::is_nothrow_move_constructible_v<mean_field::models::StellarModel>);
STATIC_CHECK_FALSE(std::derived_from<StellarModelTestStructure, mean_field::models::structure::StructureBase>);
STATIC_CHECK_FALSE(
std::derived_from<
mean_field::models::structure::PolytropicStructure, mean_field::models::structure::StructureBase>
);
STATIC_CHECK(
std::same_as<
decltype(std::declval<const mean_field::models::structure::StructureBase &>().equationOfState()),
mean_field::eos::EquationOfStateView>
);
STATIC_REQUIRE(std::is_nothrow_move_assignable_v<mean_field::models::StellarModel>);
STATIC_REQUIRE_FALSE(std::is_copy_constructible_v<PolytropicStellarModel>);
STATIC_REQUIRE_FALSE(std::is_copy_assignable_v<PolytropicStellarModel>);
STATIC_REQUIRE(std::is_nothrow_move_constructible_v<PolytropicStellarModel>);
STATIC_REQUIRE(std::is_nothrow_move_assignable_v<PolytropicStellarModel>);
mean_field::models::StellarModel model{
mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{3.0, 0.25}, 1.0},
mean_field::surface::Isobaric{0.0}
mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.0}}
};
CHECK(model.targetMass() == 1.0);
CHECK(model.resolvedSurfaceCondition().targetEnthalpy == 0.0);
CHECK(
dynamic_cast<const mean_field::models::structure::PolytropicStructure *>(&model.structurePrescription()) !=
nullptr
STATIC_CHECK(std::same_as<decltype(model), PolytropicStellarModel>);
STATIC_CHECK(
std::same_as<
decltype(model.structurePrescription()), const mean_field::models::structure::PolytropicStructure &>
);
STATIC_CHECK(
std::same_as<decltype(model.surfacePrescription()), const mean_field::surface::ConstantPressureSurface &>
);
STATIC_CHECK(std::same_as<decltype(model.equationOfState()), const mean_field::eos::Polytrope &>);
CHECK(dynamic_cast<const mean_field::surface::Isobaric *>(&model.surfacePrescription()) != nullptr);
CHECK(model.targetMass() == 1.0);
CHECK(model.compiledSurfaceConstraint().targetPressure() == mean_field::eos::PressureValue{0.0});
CHECK(&model.equationOfState() == &model.structurePrescription().equationOfState());
CHECK(model.surfacePrescription().targetPressure() == mean_field::eos::PressureValue{0.0});
}
TEST_CASE(
@@ -135,7 +157,7 @@ TEST_CASE(
) {
mean_field::models::StellarModel model{
mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{3.0, 0.25}, 1.0},
mean_field::surface::Isobaric{}
mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.0}}
};
const mean_field::models::structure::StructureSeed seed =
@@ -153,45 +175,49 @@ TEST_CASE(
TEST_CASE(
"Moving A Stellar Model Preserves Stable Prescription Addresses",
tags::barotrope &tags::unit &tags::model
tags::barotrope &tags::unit &tags::model &tags::surface_constraint_lifetime
) {
mean_field::models::StellarModel originalModel{
mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{3.0, 0.25}, 1.0},
mean_field::surface::Isobaric{}
mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.0}}
};
const mean_field::models::structure::StructureBase *structureAddress = &originalModel.structurePrescription();
const mean_field::models::structure::PolytropicStructure *structureAddress = &originalModel.structurePrescription();
const mean_field::surface::SurfaceBase *surfaceAddress = &originalModel.surfacePrescription();
const mean_field::surface::ConstantPressureSurface *surfaceAddress = &originalModel.surfacePrescription();
const mean_field::eos::EquationOfState *equationOfStateAddress = &originalModel.equationOfState();
const mean_field::eos::Polytrope *equationOfStateAddress = &originalModel.equationOfState();
const auto *compiledSurfaceConstraintAddress = &originalModel.compiledSurfaceConstraint();
mean_field::models::StellarModel movedModel{std::move(originalModel)};
CHECK(&movedModel.structurePrescription() == structureAddress);
CHECK(&movedModel.surfacePrescription() == surfaceAddress);
CHECK(&movedModel.equationOfState() == equationOfStateAddress);
CHECK(&movedModel.compiledSurfaceConstraint() == compiledSurfaceConstraintAddress);
CHECK(movedModel.targetMass() == 1.0);
}
TEST_CASE(
"Stellar Model Resolves A Positive Isobaric Surface",
"Stellar Model Compiles A Positive Constant Pressure Surface",
tags::barotrope &tags::unit &tags::model
) {
constexpr double targetPressure = 0.03125;
mean_field::models::StellarModel model{
mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{3.0, 0.25}, 1.0},
mean_field::surface::Isobaric{targetPressure}
mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{targetPressure}}
};
const double targetEnthalpy = model.resolvedSurfaceCondition().targetEnthalpy;
const double requiredSpecificEnthalpy = mean_field::eos::evaluate<mean_field::eos::quantity::SpecificEnthalpy>(
model.equationOfState(), mean_field::eos::PressureValue{targetPressure}
)
.value();
CHECK(targetEnthalpy > 0.0);
CHECK(
std::abs(model.equationOfState().pressure_from_enthalpy(targetEnthalpy) - targetPressure) <
64.0 * std::numeric_limits<double>::epsilon()
);
CHECK(requiredSpecificEnthalpy > 0.0);
CHECK(model.compiledSurfaceConstraint().targetPressure() == mean_field::eos::PressureValue{targetPressure});
CHECK(model.compiledSurfaceConstraint().residual(ModelSurfaceState{requiredSpecificEnthalpy}) == 0.0);
}
TEST_CASE(
@@ -200,27 +226,22 @@ TEST_CASE(
) {
const auto tracker = std::make_shared<StellarModelExtensionTracker>();
mean_field::models::StellarModel model{StellarModelTestStructure{tracker}, StellarModelTestSurface{tracker}};
mean_field::models::StellarModel model{
StellarModelTestStructure{tracker},
mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.375}}
};
STATIC_CHECK(std::same_as<decltype(model), ExtensionStellarModel>);
REQUIRE(tracker->structureValidationCount == 1);
REQUIRE(tracker->surfaceValidationCount == 1);
REQUIRE(tracker->surfaceResolutionCount == 1);
CHECK(dynamic_cast<const StellarModelTestStructure *>(&model.structurePrescription()) != nullptr);
CHECK(dynamic_cast<const StellarModelTestSurface *>(&model.surfacePrescription()) != nullptr);
const mean_field::eos::EquationOfState *ownedEquationOfState = &model.equationOfState();
const mean_field::eos::Polytrope *ownedEquationOfState = &model.equationOfState();
CHECK(tracker->structureEquationOfState == ownedEquationOfState);
CHECK(tracker->surfaceValidationEquationOfState == ownedEquationOfState);
CHECK(tracker->surfaceResolutionEquationOfState == ownedEquationOfState);
CHECK(model.targetMass() == 2.5);
CHECK(model.resolvedSurfaceCondition().targetEnthalpy == 0.375);
CHECK(model.compiledSurfaceConstraint().targetPressure() == mean_field::eos::PressureValue{0.375});
}
TEST_CASE(
@@ -229,23 +250,25 @@ TEST_CASE(
) {
mean_field::models::StellarModel sourceModel{
mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{3.0, 0.25}, 1.25},
mean_field::surface::Isobaric{0.0}
mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.0}}
};
mean_field::models::StellarModel destinationModel{
mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{2.0, 0.5}, 4.0},
mean_field::surface::Isobaric{0.02}
mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.02}}
};
const mean_field::models::structure::StructureBase *sourceStructureAddress = &sourceModel.structurePrescription();
const mean_field::models::structure::PolytropicStructure *sourceStructureAddress =
&sourceModel.structurePrescription();
const mean_field::surface::SurfaceBase *sourceSurfaceAddress = &sourceModel.surfacePrescription();
const mean_field::surface::ConstantPressureSurface *sourceSurfaceAddress = &sourceModel.surfacePrescription();
const mean_field::eos::EquationOfState *sourceEquationOfStateAddress = &sourceModel.equationOfState();
const mean_field::eos::Polytrope *sourceEquationOfStateAddress = &sourceModel.equationOfState();
const double sourceTargetEnthalpy = sourceModel.resolvedSurfaceCondition().targetEnthalpy;
const mean_field::eos::PressureValue sourceTargetPressure =
sourceModel.compiledSurfaceConstraint().targetPressure();
destinationModel = std::move(sourceModel);
destinationModel = std::move(sourceModel);
CHECK(&destinationModel.structurePrescription() == sourceStructureAddress);
@@ -255,5 +278,83 @@ TEST_CASE(
CHECK(destinationModel.targetMass() == 1.25);
CHECK(destinationModel.resolvedSurfaceCondition().targetEnthalpy == sourceTargetEnthalpy);
}
CHECK(destinationModel.compiledSurfaceConstraint().targetPressure() == sourceTargetPressure);
}
TEST_CASE(
"Stellar Model View Supports Heterogeneous Typed Models",
tags::stellar_model_runtime_view
) {
const auto tracker = std::make_shared<StellarModelExtensionTracker>();
const mean_field::models::StellarModel polytropicModel{
mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{3.0, 0.25}, 1.0},
mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.0}}
};
const mean_field::models::StellarModel extensionModel{
StellarModelTestStructure{tracker},
mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.375}}
};
STATIC_CHECK(std::is_trivially_copyable_v<mean_field::models::StellarModelView>);
STATIC_CHECK_FALSE(std::constructible_from<mean_field::models::StellarModelView, PolytropicStellarModel &&>);
const std::array views{
mean_field::models::StellarModelView{polytropicModel}, mean_field::models::StellarModelView{extensionModel}
};
CHECK(views[0].targetMass() == 1.0);
CHECK(views[1].targetMass() == 2.5);
CHECK(views[1].surfaceCondition().targetPressure == 0.375);
REQUIRE(views[1].surfaceDependencies().stateFields.size() == 1);
CHECK(
views[1].surfaceDependencies().residualRowField ==
mean_field::surface::surfaceFieldId<mean_field::field::Enthalpy>
);
const auto pressure =
views[0].equationOfState().tryEvaluate<mean_field::eos::quantity::Pressure>(mean_field::eos::DensityValue{0.7});
REQUIRE(pressure.has_value());
CHECK(
pressure->value() == mean_field::eos::evaluate<mean_field::eos::quantity::Pressure>(
polytropicModel.equationOfState(), mean_field::eos::DensityValue{0.7}
)
.value()
);
const mean_field::models::structure::StructureSeed seed =
views[1].makeInitialSeed({.centralDensity = 1.75, .radialSampleCount = 2});
CHECK(seed.centralDensity == 1.75);
CHECK(seed.radius.Size() == 2);
}
TEST_CASE(
"Stellar Model View Retains Stable Pointees When Its Owner Moves",
tags::stellar_model_runtime_view
) {
mean_field::models::StellarModel originalModel{
mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{3.0, 0.25}, 1.0},
mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.0}}
};
const mean_field::models::StellarModelView view{originalModel};
PolytropicStellarModel movedModel{std::move(originalModel)};
const auto pressure =
view.equationOfState().tryEvaluate<mean_field::eos::quantity::Pressure>(mean_field::eos::DensityValue{0.7});
const mean_field::models::structure::StructureSeed seed =
view.makeInitialSeed({.centralDensity = 1.0, .radialSampleCount = 8});
REQUIRE(pressure.has_value());
CHECK(view.targetMass() == movedModel.targetMass());
CHECK(
pressure->value() == mean_field::eos::evaluate<mean_field::eos::quantity::Pressure>(
movedModel.equationOfState(), mean_field::eos::DensityValue{0.7}
)
.value()
);
CHECK(seed.radius.Size() == 8);
}