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

@@ -8,115 +8,202 @@
import mean_field;
import test_helpers;
TEST_CASE("Polytropic EOS Satisfies Its Analytic Identities",
tags::barotrope_eos_unit) {
constexpr double polytropic_index = 3.0;
constexpr double polytropic_constant = 1.5;
TEST_CASE(
"Polytropic EOS Satisfies Its Analytic Identities",
tags::barotrope_eos_unit
) {
using namespace mean_field::eos;
const mean_field::eos::Polytrope barotrope(polytropic_index,
polytropic_constant);
constexpr double polytropic_index = 3.0;
constexpr double polytropic_constant = 1.5;
const std::array<double, 5> densities{1.0e-6, 1.0e-3, 0.1, 0.7, 2.0};
const Polytrope barotrope(polytropic_index, polytropic_constant);
for (const double density : densities) {
const double pressure = barotrope.pressure_from_density(density);
using densityV = DensityValue;
using pressureV = PressureValue;
using enthalpyV = SpecificEnthalpyValue;
const double enthalpy = barotrope.enthalpy_from_density(density);
constexpr std::array<densityV, 5> densities{
densityV{1.0e-6}, densityV{1.0e-3}, densityV{0.1}, densityV{0.7}, densityV{2.0}
};
const double reconstructed_density =
barotrope.density_from_enthalpy(enthalpy);
for (const densityV density : densities) {
const pressureV pressure = evaluate<quantity::Pressure>(barotrope, density);
const double reconstructed_pressure =
barotrope.pressure_from_enthalpy(enthalpy);
const enthalpyV enthalpy = evaluate<quantity::SpecificEnthalpy>(barotrope, density);
const densityV reconstructed_density = evaluate<quantity::Density>(barotrope, enthalpy);
const double reconstructed_enthalpy =
barotrope.enthalpy_from_pressure(pressure);
const pressureV reconstructed_pressure = evaluate<quantity::Pressure>(barotrope, enthalpy);
CHECK_THAT(reconstructed_density,
Catch::Matchers::WithinRel(density, 2.0e-14));
const enthalpyV reconstructed_enthalpy = evaluate<quantity::SpecificEnthalpy>(barotrope, pressure);
CHECK_THAT(reconstructed_pressure,
Catch::Matchers::WithinRel(pressure, 2.0e-14));
CHECK_THAT(reconstructed_density.value(), Catch::Matchers::WithinRel(density.value(), 2.0e-14));
CHECK_THAT(reconstructed_enthalpy,
Catch::Matchers::WithinRel(enthalpy, 2.0e-14));
CHECK_THAT(reconstructed_pressure.value(), Catch::Matchers::WithinRel(pressure.value(), 2.0e-14));
CHECK_THAT(pressure,
Catch::Matchers::WithinRel(
density * enthalpy / (polytropic_index + 1.0), 2.0e-14));
CHECK_THAT(reconstructed_enthalpy.value(), Catch::Matchers::WithinRel(enthalpy.value(), 2.0e-14));
CHECK_THAT(barotrope.pressure_derivative_from_enthalpy(enthalpy),
Catch::Matchers::WithinRel(density, 2.0e-14));
CHECK_THAT(
pressure.value(),
Catch::Matchers::WithinRel(density.value() * enthalpy.value() / (polytropic_index + 1.0), 2.0e-14)
);
CHECK_THAT(
barotrope.pressure_derivative_from_density(density),
Catch::Matchers::WithinRel(enthalpy / polytropic_index, 2.0e-14));
}
CHECK_THAT(
(mean_field::eos::partialDerivative<
mean_field::eos::quantity::Pressure, mean_field::eos::quantity::SpecificEnthalpy>(
barotrope, mean_field::eos::SpecificEnthalpyValue{enthalpy}
)
.value()),
Catch::Matchers::WithinRel(density.value(), 2.0e-14)
);
CHECK_THAT(
(mean_field::eos::partialDerivative<
mean_field::eos::quantity::Pressure, mean_field::eos::quantity::Density>(
barotrope, mean_field::eos::DensityValue{density}
)
.value()),
Catch::Matchers::WithinRel(enthalpy.value() / polytropic_index, 2.0e-14)
);
}
}
TEST_CASE("Polytropic EOS Derivatives Match Centered Differences",
tags::barotrope_eos_jacobian) {
const mean_field::eos::Polytrope barotrope(3.0, 1.5);
TEST_CASE(
"Polytropic EOS Derivatives Match Centered Differences",
tags::barotrope_eos_jacobian
) {
using namespace mean_field::eos;
const std::array<double, 4> enthalpies{0.05, 0.2, 0.7, 1.4};
const Polytrope barotrope(3.0, 1.5);
for (const double enthalpy : enthalpies) {
const double step = 1.0e-6 * std::max(1.0, enthalpy);
using densityV = DensityValue;
using pressureV = PressureValue;
using enthalpyV = SpecificEnthalpyValue;
const double density_difference =
(barotrope.density_from_enthalpy(enthalpy + step) -
barotrope.density_from_enthalpy(enthalpy - step)) /
(2.0 * step);
constexpr std::array<enthalpyV, 4> enthalpies{enthalpyV{0.05}, enthalpyV{0.2}, enthalpyV{0.7}, enthalpyV{1.4}};
const double pressure_difference =
(barotrope.pressure_from_enthalpy(enthalpy + step) -
barotrope.pressure_from_enthalpy(enthalpy - step)) /
(2.0 * step);
for (const enthalpyV enthalpy : enthalpies) {
const enthalpyV step = enthalpyV{1.0e-6} * std::max(1.0, enthalpy.value());
CHECK_THAT(
density_difference,
Catch::Matchers::WithinRel(
barotrope.density_derivative_from_enthalpy(enthalpy), 5.0e-10));
const densityV density_difference = (evaluate<quantity::Density>(barotrope, enthalpy + step) -
evaluate<quantity::Density>(barotrope, enthalpy - step)) /
(2.0 * step.value());
CHECK_THAT(
pressure_difference,
Catch::Matchers::WithinRel(
barotrope.pressure_derivative_from_enthalpy(enthalpy), 5.0e-10));
}
const pressureV pressure_difference = (evaluate<quantity::Pressure>(barotrope, enthalpy + step) -
evaluate<quantity::Pressure>(barotrope, enthalpy - step)) /
(2.0 * step.value());
CHECK_THAT(
density_difference.value(),
Catch::Matchers::WithinRel(
mean_field::eos::partialDerivative<
mean_field::eos::quantity::Density, mean_field::eos::quantity::SpecificEnthalpy>(
barotrope, enthalpy
)
.value(),
5.0e-10
)
);
CHECK_THAT(
pressure_difference.value(),
Catch::Matchers::WithinRel(
mean_field::eos::partialDerivative<
mean_field::eos::quantity::Pressure, mean_field::eos::quantity::SpecificEnthalpy>(
barotrope, enthalpy
)
.value(),
5.0e-10
)
);
}
}
TEST_CASE("Polytropic EOS Has An Exact Zero Density Surface",
tags::barotrope_eos_unit) {
const mean_field::eos::Polytrope barotrope(3.0, 1.5);
TEST_CASE(
"Polytropic EOS Has An Exact Zero Density Surface",
tags::barotrope_eos_unit
) {
const mean_field::eos::Polytrope barotrope(3.0, 1.5);
CHECK(barotrope.density_from_enthalpy(-1.0) == 0.0);
CHECK(barotrope.density_from_enthalpy(0.0) == 0.0);
CHECK(
mean_field::eos::evaluate<mean_field::eos::quantity::Density>(
barotrope, mean_field::eos::SpecificEnthalpyValue{-1.0}
)
.value() == 0.0
);
CHECK(
mean_field::eos::evaluate<mean_field::eos::quantity::Density>(
barotrope, mean_field::eos::SpecificEnthalpyValue{0.0}
)
.value() == 0.0
);
CHECK(barotrope.pressure_from_enthalpy(-1.0) == 0.0);
CHECK(barotrope.pressure_from_enthalpy(0.0) == 0.0);
CHECK(
mean_field::eos::evaluate<mean_field::eos::quantity::Pressure>(
barotrope, mean_field::eos::SpecificEnthalpyValue{-1.0}
)
.value() == 0.0
);
CHECK(
mean_field::eos::evaluate<mean_field::eos::quantity::Pressure>(
barotrope, mean_field::eos::SpecificEnthalpyValue{0.0}
)
.value() == 0.0
);
CHECK(barotrope.density_derivative_from_enthalpy(-1.0) == 0.0);
CHECK(
(mean_field::eos::partialDerivative<
mean_field::eos::quantity::Density, mean_field::eos::quantity::SpecificEnthalpy>(
barotrope, mean_field::eos::SpecificEnthalpyValue{-1.0}
)
.value() == 0.0)
);
CHECK(barotrope.density_derivative_from_enthalpy(0.0) == 0.0);
CHECK(
(mean_field::eos::partialDerivative<
mean_field::eos::quantity::Density, mean_field::eos::quantity::SpecificEnthalpy>(
barotrope, mean_field::eos::SpecificEnthalpyValue{0.0}
)
.value() == 0.0)
);
CHECK(barotrope.pressure_derivative_from_enthalpy(0.0) == 0.0);
CHECK(
(mean_field::eos::partialDerivative<
mean_field::eos::quantity::Pressure, mean_field::eos::quantity::SpecificEnthalpy>(
barotrope, mean_field::eos::SpecificEnthalpyValue{0.0}
)
.value() == 0.0)
);
}
TEST_CASE("Polytropic EOS Rejects Invalid Material Parameters",
tags::barotrope_eos_unit) {
CHECK_THROWS_AS(mean_field::eos::Polytrope(0.5, 1.0), std::invalid_argument);
TEST_CASE(
"Polytropic EOS Rejects Invalid Material Parameters",
tags::barotrope_eos_unit
) {
CHECK_THROWS_AS(mean_field::eos::Polytrope(0.5, 1.0), std::invalid_argument);
CHECK_THROWS_AS(mean_field::eos::Polytrope(3.0, 0.0), std::invalid_argument);
CHECK_THROWS_AS(mean_field::eos::Polytrope(3.0, 0.0), std::invalid_argument);
CHECK_THROWS_AS(
mean_field::eos::Polytrope(std::numeric_limits<double>::infinity(), 1.0),
std::invalid_argument);
CHECK_THROWS_AS(mean_field::eos::Polytrope(std::numeric_limits<double>::infinity(), 1.0), std::invalid_argument);
const mean_field::eos::Polytrope barotrope(3.0, 1.0);
const mean_field::eos::Polytrope barotrope(3.0, 1.0);
CHECK_THROWS_AS(barotrope.pressure_from_density(-1.0), std::domain_error);
CHECK_THROWS_AS(
mean_field::eos::evaluate<mean_field::eos::quantity::Pressure>(barotrope, mean_field::eos::DensityValue{-1.0}),
std::domain_error
);
CHECK_THROWS_AS(barotrope.enthalpy_from_density(-1.0), std::domain_error);
CHECK_THROWS_AS(
mean_field::eos::evaluate<mean_field::eos::quantity::SpecificEnthalpy>(
barotrope, mean_field::eos::DensityValue{-1.0}
),
std::domain_error
);
CHECK_THROWS_AS(barotrope.enthalpy_from_pressure(-1.0), std::domain_error);
CHECK_THROWS_AS(
mean_field::eos::evaluate<mean_field::eos::quantity::SpecificEnthalpy>(
barotrope, mean_field::eos::PressureValue{-1.0}
),
std::domain_error
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,125 @@
#include <catch2/catch_test_macros.hpp>
import mean_field;
import test_helpers;
namespace {
namespace eos = mean_field::eos;
class DensityClosureEquationOfState final {
public:
using Relations = eos::RelationCatalog<eos::DensityFromSpecificEnthalpy>;
[[nodiscard]] constexpr eos::DensityValue evaluate(
eos::DensityFromSpecificEnthalpy,
const eos::SpecificEnthalpyValue specificEnthalpy
) const noexcept {
return eos::DensityValue{specificEnthalpy.value()};
}
[[nodiscard]] constexpr eos::PartialDerivative<
eos::quantity::Density,
eos::quantity::SpecificEnthalpy>
partialDerivative(
eos::DensityFromSpecificEnthalpy,
eos::WithRespectTo<eos::quantity::SpecificEnthalpy>,
eos::SpecificEnthalpyValue
) const noexcept {
return eos::PartialDerivative<eos::quantity::Density, eos::quantity::SpecificEnthalpy>{1.0};
}
};
class DensityClosureWithoutDerivative final {
public:
using Relations = eos::RelationCatalog<eos::DensityFromSpecificEnthalpy>;
[[nodiscard]] constexpr eos::DensityValue evaluate(
eos::DensityFromSpecificEnthalpy,
const eos::SpecificEnthalpyValue specificEnthalpy
) const noexcept {
return eos::DensityValue{specificEnthalpy.value()};
}
};
class EnthalpyPressureEquationOfState final {
public:
using Relations = eos::RelationCatalog<eos::PressureFromSpecificEnthalpy>;
[[nodiscard]] constexpr eos::PressureValue evaluate(
eos::PressureFromSpecificEnthalpy,
const eos::SpecificEnthalpyValue specificEnthalpy
) const noexcept {
return eos::PressureValue{2.0 * specificEnthalpy.value()};
}
[[nodiscard]] constexpr eos::PartialDerivative<
eos::quantity::Pressure,
eos::quantity::SpecificEnthalpy>
partialDerivative(
eos::PressureFromSpecificEnthalpy,
eos::WithRespectTo<eos::quantity::SpecificEnthalpy>,
eos::SpecificEnthalpyValue
) const noexcept {
return eos::PartialDerivative<eos::quantity::Pressure, eos::quantity::SpecificEnthalpy>{2.0};
}
};
class DensitySeedEquationOfState final {
public:
using Relations = eos::RelationCatalog<eos::SpecificEnthalpyFromDensity>;
[[nodiscard]] constexpr eos::SpecificEnthalpyValue evaluate(
eos::SpecificEnthalpyFromDensity,
const eos::DensityValue density
) const noexcept {
return eos::SpecificEnthalpyValue{3.0 * density.value()};
}
};
class GeneralEquationOfStateWithoutCurrentConsumerRelations final {
public:
using Relations = eos::RelationCatalog<eos::SpecificEnthalpyFromPressure>;
[[nodiscard]] constexpr eos::SpecificEnthalpyValue evaluate(
eos::SpecificEnthalpyFromPressure,
const eos::PressureValue pressure
) const noexcept {
return eos::SpecificEnthalpyValue{pressure.value()};
}
};
} // namespace
TEST_CASE(
"Barotropic Closure EOS Requires Density And Its Enthalpy Derivative",
tags::barotropic_closure_equation_of_state_contract
) {
STATIC_CHECK(eos::BarotropicClosureEquationOfState<eos::Polytrope>);
STATIC_CHECK(eos::BarotropicClosureEquationOfState<DensityClosureEquationOfState>);
STATIC_CHECK(eos::EquationOfStateModel<DensityClosureWithoutDerivative>);
STATIC_CHECK_FALSE(eos::BarotropicClosureEquationOfState<DensityClosureWithoutDerivative>);
STATIC_CHECK_FALSE(eos::BarotropicClosureEquationOfState<EnthalpyPressureEquationOfState>);
}
TEST_CASE(
"Pressure Force EOS Requires Pressure And Its Enthalpy Derivative",
tags::pressure_force_equation_of_state_contract
) {
STATIC_CHECK(eos::PressureForceEquationOfState<eos::Polytrope>);
STATIC_CHECK(eos::PressureForceEquationOfState<EnthalpyPressureEquationOfState>);
STATIC_CHECK_FALSE(eos::PressureForceEquationOfState<DensityClosureEquationOfState>);
STATIC_CHECK_FALSE(eos::PressureForceEquationOfState<DensityClosureWithoutDerivative>);
}
TEST_CASE(
"Structure Seed EOS Requires Enthalpy From Density",
tags::structure_seed_equation_of_state_contract
) {
STATIC_CHECK(eos::StructureSeedEquationOfState<eos::Polytrope>);
STATIC_CHECK(eos::StructureSeedEquationOfState<DensitySeedEquationOfState>);
STATIC_CHECK_FALSE(eos::StructureSeedEquationOfState<DensityClosureEquationOfState>);
STATIC_CHECK(eos::EquationOfStateModel<GeneralEquationOfStateWithoutCurrentConsumerRelations>);
STATIC_CHECK_FALSE(eos::StructureSeedEquationOfState<GeneralEquationOfStateWithoutCurrentConsumerRelations>);
STATIC_CHECK_FALSE(eos::BarotropicClosureEquationOfState<GeneralEquationOfStateWithoutCurrentConsumerRelations>);
STATIC_CHECK_FALSE(eos::PressureForceEquationOfState<GeneralEquationOfStateWithoutCurrentConsumerRelations>);
}

View File

@@ -0,0 +1,322 @@
#include <array>
#include <concepts>
#include <expected>
#include <limits>
#include <memory>
#include <span>
#include <string_view>
#include <type_traits>
#include <catch2/catch_test_macros.hpp>
import mean_field;
import test_helpers;
namespace {
namespace eos = mean_field::eos;
class LinearPressureEquationOfState final {
public:
using Relations = eos::RelationCatalog<eos::PressureFromDensity>;
[[nodiscard]] constexpr eos::PressureValue evaluate(
eos::PressureFromDensity,
const eos::DensityValue density
) const noexcept {
return eos::PressureValue{2.0 * density.value() + 0.5};
}
};
struct DensityAlias final : eos::ThermodynamicQuantity {
static constexpr std::string_view identifier = "density";
};
class AmbiguouslyIdentifiedEquationOfState final {
public:
using Relations = eos::RelationCatalog<eos::Relation<DensityAlias, eos::quantity::Density>>;
[[nodiscard]] constexpr eos::QuantityValue<DensityAlias> evaluate(
eos::Relation<
DensityAlias,
eos::quantity::Density>,
const eos::DensityValue density
) const noexcept {
return eos::QuantityValue<DensityAlias>{density.value()};
}
};
[[nodiscard]] std::expected<
eos::PressureValue,
eos::EvaluationError>
pressureAtDensity(
const eos::EquationOfStateView equationOfState,
const eos::DensityValue density
) {
return equationOfState.tryEvaluate<eos::quantity::Pressure>(density);
}
[[nodiscard]] const eos::RuntimeRelationDescriptor *findRelation(
const eos::EquationOfStateView equationOfState,
const eos::ThermodynamicQuantityId output,
const eos::ThermodynamicQuantityId input
) {
for (const eos::RuntimeRelationDescriptor &relation : equationOfState.relations()) {
if (relation.outputQuantity == output && relation.inputQuantities.size() == 1 &&
relation.inputQuantities[0] == input) {
return std::addressof(relation);
}
}
return nullptr;
}
} // namespace
TEST_CASE(
"Runtime EOS View Generates The Polytropic Relation Catalog",
tags::equation_of_state_runtime_contract
) {
STATIC_CHECK(eos::RuntimeEquationOfStateModel<eos::Polytrope>);
STATIC_CHECK(eos::RuntimeEquationOfStateModel<LinearPressureEquationOfState>);
STATIC_CHECK(eos::EquationOfStateModel<AmbiguouslyIdentifiedEquationOfState>);
STATIC_CHECK_FALSE(eos::RuntimeEquationOfStateModel<AmbiguouslyIdentifiedEquationOfState>);
STATIC_CHECK(std::is_trivially_copyable_v<eos::EquationOfStateView>);
STATIC_CHECK_FALSE(std::constructible_from<eos::EquationOfStateView, eos::Polytrope &&>);
const eos::Polytrope equationOfState(3.0, 0.25);
const eos::Polytrope secondEquationOfState(1.5, 0.73);
const eos::EquationOfStateView view{equationOfState};
const eos::EquationOfStateView secondView{secondEquationOfState};
REQUIRE(view.relations().size() == eos::Polytrope::Relations::size);
CHECK(view.relations().data() == secondView.relations().data());
CHECK(eos::thermodynamicQuantityId<eos::quantity::Density>.name() == "density");
CHECK(eos::thermodynamicQuantityId<eos::quantity::Pressure>.name() == "pressure");
CHECK(eos::thermodynamicQuantityId<eos::quantity::SpecificEnthalpy>.name() == "specific_enthalpy");
const eos::RuntimeRelationDescriptor *pressureFromDensity = findRelation(
view, eos::thermodynamicQuantityId<eos::quantity::Pressure>,
eos::thermodynamicQuantityId<eos::quantity::Density>
);
REQUIRE(pressureFromDensity != nullptr);
CHECK(pressureFromDensity->hasPartialDerivative(0));
const eos::RuntimeRelationDescriptor *specificEnthalpyFromPressure = findRelation(
view, eos::thermodynamicQuantityId<eos::quantity::SpecificEnthalpy>,
eos::thermodynamicQuantityId<eos::quantity::Pressure>
);
REQUIRE(specificEnthalpyFromPressure != nullptr);
CHECK_FALSE(specificEnthalpyFromPressure->hasPartialDerivative(0));
const eos::RuntimeRelationDescriptor *pressureFromSpecificEnthalpy = findRelation(
view, eos::thermodynamicQuantityId<eos::quantity::Pressure>,
eos::thermodynamicQuantityId<eos::quantity::SpecificEnthalpy>
);
REQUIRE(pressureFromSpecificEnthalpy != nullptr);
CHECK(pressureFromSpecificEnthalpy->hasPartialDerivative(0));
const eos::RuntimeRelationDescriptor *specificEnthalpyFromDensity = findRelation(
view, eos::thermodynamicQuantityId<eos::quantity::SpecificEnthalpy>,
eos::thermodynamicQuantityId<eos::quantity::Density>
);
REQUIRE(specificEnthalpyFromDensity != nullptr);
CHECK_FALSE(specificEnthalpyFromDensity->hasPartialDerivative(0));
const eos::RuntimeRelationDescriptor *densityFromSpecificEnthalpy = findRelation(
view, eos::thermodynamicQuantityId<eos::quantity::Density>,
eos::thermodynamicQuantityId<eos::quantity::SpecificEnthalpy>
);
REQUIRE(densityFromSpecificEnthalpy != nullptr);
CHECK(densityFromSpecificEnthalpy->hasPartialDerivative(0));
}
TEST_CASE(
"Runtime EOS View Matches Typed Polytropic Evaluation",
tags::equation_of_state_runtime_compatibility
) {
const eos::Polytrope equationOfState(3.0, 0.25);
const eos::EquationOfStateView view{equationOfState};
const eos::DensityValue density{0.7};
const eos::SpecificEnthalpyValue specificEnthalpy{0.9};
const eos::PressureValue pressure{0.04};
const auto runtimePressureFromDensity = view.tryEvaluate<eos::quantity::Pressure>(density);
const auto runtimePressureFromSpecificEnthalpy = view.tryEvaluate<eos::quantity::Pressure>(specificEnthalpy);
const auto runtimeSpecificEnthalpyFromDensity = view.tryEvaluate<eos::quantity::SpecificEnthalpy>(density);
const auto runtimeSpecificEnthalpyFromPressure = view.tryEvaluate<eos::quantity::SpecificEnthalpy>(pressure);
const auto runtimeDensityFromSpecificEnthalpy = view.tryEvaluate<eos::quantity::Density>(specificEnthalpy);
REQUIRE(runtimePressureFromDensity.has_value());
REQUIRE(runtimePressureFromSpecificEnthalpy.has_value());
REQUIRE(runtimeSpecificEnthalpyFromDensity.has_value());
REQUIRE(runtimeSpecificEnthalpyFromPressure.has_value());
REQUIRE(runtimeDensityFromSpecificEnthalpy.has_value());
CHECK(
runtimePressureFromDensity->value() == eos::evaluate<eos::quantity::Pressure>(equationOfState, density).value()
);
CHECK(
runtimePressureFromSpecificEnthalpy->value() ==
eos::evaluate<eos::quantity::Pressure>(equationOfState, specificEnthalpy).value()
);
CHECK(
runtimeSpecificEnthalpyFromDensity->value() ==
eos::evaluate<eos::quantity::SpecificEnthalpy>(equationOfState, density).value()
);
CHECK(
runtimeSpecificEnthalpyFromPressure->value() ==
eos::evaluate<eos::quantity::SpecificEnthalpy>(equationOfState, pressure).value()
);
CHECK(
runtimeDensityFromSpecificEnthalpy->value() ==
eos::evaluate<eos::quantity::Density>(equationOfState, specificEnthalpy).value()
);
const std::array runtimeDensityInput{
eos::RuntimeQuantityValue{eos::thermodynamicQuantityId<eos::quantity::Density>, density.value()}
};
const auto erasedPressureFromDensity = view.tryEvaluate(
eos::thermodynamicQuantityId<eos::quantity::Pressure>,
std::span<const eos::RuntimeQuantityValue>{runtimeDensityInput}
);
REQUIRE(erasedPressureFromDensity.has_value());
CHECK(erasedPressureFromDensity->quantity == eos::thermodynamicQuantityId<eos::quantity::Pressure>);
CHECK(erasedPressureFromDensity->value == runtimePressureFromDensity->value());
const auto runtimePressureDerivative =
view.tryPartialDerivative<eos::quantity::Pressure, eos::quantity::SpecificEnthalpy>(specificEnthalpy);
const auto runtimeDensityDerivative =
view.tryPartialDerivative<eos::quantity::Density, eos::quantity::SpecificEnthalpy>(specificEnthalpy);
const auto runtimePressureDensityDerivative =
view.tryPartialDerivative<eos::quantity::Pressure, eos::quantity::Density>(density);
REQUIRE(runtimePressureDerivative.has_value());
REQUIRE(runtimeDensityDerivative.has_value());
REQUIRE(runtimePressureDensityDerivative.has_value());
CHECK(
runtimePressureDerivative->value() ==
eos::partialDerivative<eos::quantity::Pressure, eos::quantity::SpecificEnthalpy>(
equationOfState, specificEnthalpy
)
.value()
);
CHECK(
runtimeDensityDerivative->value() ==
eos::partialDerivative<eos::quantity::Density, eos::quantity::SpecificEnthalpy>(
equationOfState, specificEnthalpy
)
.value()
);
CHECK(
runtimePressureDensityDerivative->value() ==
eos::partialDerivative<eos::quantity::Pressure, eos::quantity::Density>(equationOfState, density).value()
);
const auto erasedPressureDensityDerivative = view.tryPartialDerivative(
eos::thermodynamicQuantityId<eos::quantity::Pressure>, eos::thermodynamicQuantityId<eos::quantity::Density>,
std::span<const eos::RuntimeQuantityValue>{runtimeDensityInput}
);
REQUIRE(erasedPressureDensityDerivative.has_value());
CHECK(*erasedPressureDensityDerivative == runtimePressureDensityDerivative->value());
}
TEST_CASE(
"Runtime EOS View Reports Unsupported And Invalid Requests",
tags::equation_of_state_runtime_contract
) {
const eos::Polytrope equationOfState(3.0, 0.25);
const eos::EquationOfStateView view{equationOfState};
constexpr eos::ThermodynamicQuantityId temperature{"temperature"};
const std::array densityInput{eos::RuntimeQuantityValue{eos::thermodynamicQuantityId<eos::quantity::Density>, 0.7}};
const std::array pressureInput{
eos::RuntimeQuantityValue{eos::thermodynamicQuantityId<eos::quantity::Pressure>, 0.04}
};
const std::array<eos::RuntimeQuantityValue, 0> noInputs{};
const auto unsupportedOutput =
view.tryEvaluate(temperature, std::span<const eos::RuntimeQuantityValue>{densityInput});
REQUIRE_FALSE(unsupportedOutput.has_value());
CHECK(unsupportedOutput.error().code() == eos::EvaluationErrorCode::unsupported_relation);
const auto wrongInputCount = view.tryEvaluate(
eos::thermodynamicQuantityId<eos::quantity::Pressure>, std::span<const eos::RuntimeQuantityValue>{noInputs}
);
REQUIRE_FALSE(wrongInputCount.has_value());
CHECK(wrongInputCount.error().code() == eos::EvaluationErrorCode::wrong_input_count);
const auto wrongInputQuantity = view.tryEvaluate(
eos::thermodynamicQuantityId<eos::quantity::Density>, std::span<const eos::RuntimeQuantityValue>{pressureInput}
);
REQUIRE_FALSE(wrongInputQuantity.has_value());
CHECK(wrongInputQuantity.error().code() == eos::EvaluationErrorCode::wrong_input_quantity);
const auto unsupportedDerivative = view.tryPartialDerivative(
eos::thermodynamicQuantityId<eos::quantity::SpecificEnthalpy>,
eos::thermodynamicQuantityId<eos::quantity::Pressure>, std::span<const eos::RuntimeQuantityValue>{pressureInput}
);
REQUIRE_FALSE(unsupportedDerivative.has_value());
CHECK(unsupportedDerivative.error().code() == eos::EvaluationErrorCode::unsupported_derivative);
const auto invalidDensity = view.tryEvaluate<eos::quantity::Pressure>(eos::DensityValue{-0.1});
REQUIRE_FALSE(invalidDensity.has_value());
CHECK(invalidDensity.error().code() == eos::EvaluationErrorCode::outside_domain);
const auto nonfiniteDensity =
view.tryEvaluate<eos::quantity::Pressure>(eos::DensityValue{std::numeric_limits<double>::quiet_NaN()});
REQUIRE_FALSE(nonfiniteDensity.has_value());
CHECK(nonfiniteDensity.error().code() == eos::EvaluationErrorCode::nonfinite_input);
}
TEST_CASE(
"One Runtime EOS Function Accepts Heterogeneous Concrete Models",
tags::equation_of_state_runtime_compatibility
) {
const eos::Polytrope polytrope(3.0, 0.25);
const LinearPressureEquationOfState linearEquationOfState;
const std::array views{eos::EquationOfStateView{polytrope}, eos::EquationOfStateView{linearEquationOfState}};
const eos::DensityValue density{0.7};
const auto polytropicPressure = pressureAtDensity(views[0], density);
const auto linearPressure = pressureAtDensity(views[1], density);
REQUIRE(polytropicPressure.has_value());
REQUIRE(linearPressure.has_value());
CHECK(polytropicPressure->value() == eos::evaluate<eos::quantity::Pressure>(polytrope, density).value());
CHECK(linearPressure->value() == 1.9);
}
TEST_CASE(
"Runtime EOS View Remains Valid When Stable Ownership Moves",
tags::equation_of_state_runtime_contract
) {
auto owner = std::make_unique<const eos::Polytrope>(3.0, 0.25);
const eos::EquationOfStateView view{*owner};
auto movedOwner = std::move(owner);
const auto pressure = view.tryEvaluate<eos::quantity::Pressure>(eos::DensityValue{0.7});
REQUIRE(movedOwner != nullptr);
REQUIRE(pressure.has_value());
CHECK(pressure->value() == eos::evaluate<eos::quantity::Pressure>(*movedOwner, eos::DensityValue{0.7}).value());
}

View File

@@ -0,0 +1,228 @@
#include <concepts>
#include <string_view>
#include <type_traits>
#include <catch2/catch_test_macros.hpp>
import mean_field;
import test_helpers;
namespace {
namespace eos = mean_field::eos;
struct Entropy final : eos::ThermodynamicQuantity { };
struct ElectronFraction final : eos::ThermodynamicQuantity { };
using SpecificEnthalpyFromPressureAndEntropy =
eos::Relation<eos::quantity::SpecificEnthalpy, eos::quantity::Pressure, Entropy>;
class CompleteEquationOfState final {
public:
using Relations = eos::RelationCatalog<
eos::PressureFromDensity,
eos::SpecificEnthalpyFromPressure,
SpecificEnthalpyFromPressureAndEntropy>;
[[nodiscard]] constexpr eos::PressureValue evaluate(
eos::PressureFromDensity,
const eos::DensityValue density
) const noexcept {
return eos::PressureValue{2.0 * density.value()};
}
[[nodiscard]] constexpr eos::SpecificEnthalpyValue evaluate(
eos::SpecificEnthalpyFromPressure,
const eos::PressureValue pressure
) const noexcept {
return eos::SpecificEnthalpyValue{3.0 * pressure.value()};
}
[[nodiscard]] constexpr eos::SpecificEnthalpyValue evaluate(
SpecificEnthalpyFromPressureAndEntropy,
const eos::PressureValue pressure,
const eos::QuantityValue<Entropy> entropy
) const noexcept {
return eos::SpecificEnthalpyValue{3.0 * pressure.value() + 5.0 * entropy.value()};
}
[[nodiscard]] constexpr eos::PartialDerivative<
eos::quantity::SpecificEnthalpy,
Entropy>
partialDerivative(
SpecificEnthalpyFromPressureAndEntropy,
eos::WithRespectTo<Entropy>,
eos::PressureValue,
eos::QuantityValue<Entropy>
) const noexcept {
return eos::PartialDerivative<eos::quantity::SpecificEnthalpy, Entropy>{5.0};
}
};
class MissingRelationImplementation final {
public:
using Relations = eos::RelationCatalog<eos::PressureFromDensity, eos::SpecificEnthalpyFromPressure>;
[[nodiscard]] eos::PressureValue evaluate(
eos::PressureFromDensity,
eos::DensityValue density
) const {
return eos::PressureValue{density.value()};
}
};
class IncorrectRelationOutput final {
public:
using Relations = eos::RelationCatalog<eos::PressureFromDensity>;
[[nodiscard]] eos::DensityValue evaluate(
eos::PressureFromDensity,
eos::DensityValue density
) const {
return density;
}
};
class InvalidRelationCatalog final {
public:
using Relations = eos::RelationCatalog<eos::Relation<double, eos::quantity::Density>>;
};
template <typename EquationOfState>
concept CanEvaluateDensityFromSpecificEnthalpy = requires(const EquationOfState &equationOfState) {
eos::evaluate<eos::quantity::Density>(equationOfState, eos::SpecificEnthalpyValue{1.0});
};
} // namespace
TEST_CASE(
"Thermodynamic Values Preserve Physical Quantity Types",
tags::equation_of_state_quantity_types
) {
STATIC_CHECK(eos::ThermodynamicQuantityType<eos::quantity::Density>);
STATIC_CHECK(eos::ThermodynamicQuantityType<eos::quantity::Pressure>);
STATIC_CHECK(eos::ThermodynamicQuantityType<eos::quantity::SpecificEnthalpy>);
STATIC_CHECK_FALSE(eos::ThermodynamicQuantityType<const eos::quantity::Pressure>);
STATIC_CHECK_FALSE(std::same_as<eos::DensityValue, eos::PressureValue>);
STATIC_CHECK_FALSE(std::same_as<eos::PressureValue, eos::SpecificEnthalpyValue>);
STATIC_CHECK_FALSE(std::is_convertible_v<double, eos::PressureValue>);
STATIC_CHECK_FALSE(std::is_constructible_v<eos::PressureValue, eos::DensityValue>);
STATIC_CHECK(std::is_trivially_copyable_v<eos::DensityValue>);
STATIC_CHECK(std::is_standard_layout_v<eos::DensityValue>);
STATIC_CHECK(sizeof(eos::DensityValue) == sizeof(double));
STATIC_CHECK(sizeof(eos::PressureValue) == sizeof(double));
STATIC_CHECK(sizeof(eos::SpecificEnthalpyValue) == sizeof(double));
STATIC_CHECK(std::is_empty_v<eos::PressureFromDensity>);
constexpr eos::DensityValue density{-0.25};
STATIC_CHECK(density.value() == -0.25);
}
TEST_CASE(
"Thermodynamic Derivatives Preserve Numerator And Denominator Types",
tags::equation_of_state_quantity_types
) {
using PressureByDensity = eos::PartialDerivative<eos::quantity::Pressure, eos::quantity::Density>;
using PressureBySpecificEnthalpy = eos::PartialDerivative<eos::quantity::Pressure, eos::quantity::SpecificEnthalpy>;
STATIC_CHECK_FALSE(std::same_as<PressureByDensity, PressureBySpecificEnthalpy>);
STATIC_CHECK_FALSE(std::is_convertible_v<PressureByDensity, PressureBySpecificEnthalpy>);
STATIC_CHECK(std::is_trivially_copyable_v<PressureByDensity>);
STATIC_CHECK(std::is_standard_layout_v<PressureByDensity>);
STATIC_CHECK(sizeof(PressureByDensity) == sizeof(double));
constexpr PressureByDensity derivative{1.75};
STATIC_CHECK(derivative.value() == 1.75);
}
TEST_CASE(
"EOS Relation Catalog Rejects Invalid And Duplicate Relations",
tags::equation_of_state_relation_contract
) {
using ValidCatalog = eos::RelationCatalog<eos::PressureFromDensity, eos::SpecificEnthalpyFromPressure>;
using DuplicateCatalog = eos::RelationCatalog<eos::PressureFromDensity, eos::PressureFromDensity>;
using InvalidRelation = eos::Relation<double, eos::quantity::Density>;
using InvalidCatalog = eos::RelationCatalog<InvalidRelation>;
using RepeatedInputRelation =
eos::Relation<eos::quantity::Pressure, eos::quantity::Density, eos::quantity::Density>;
using RepeatedInputCatalog = eos::RelationCatalog<RepeatedInputRelation>;
STATIC_CHECK(eos::ValidRelationCatalog<ValidCatalog>);
STATIC_CHECK_FALSE(eos::ValidRelationCatalog<DuplicateCatalog>);
STATIC_CHECK_FALSE(eos::ValidRelationCatalog<InvalidCatalog>);
STATIC_CHECK_FALSE(eos::ValidRelationCatalog<RepeatedInputCatalog>);
STATIC_CHECK_FALSE(eos::ValidRelationCatalog<eos::RelationCatalog<>>);
STATIC_CHECK(eos::relationCatalogContains<ValidCatalog, eos::PressureFromDensity>);
STATIC_CHECK_FALSE(eos::relationCatalogContains<ValidCatalog, eos::DensityFromSpecificEnthalpy>);
STATIC_CHECK(eos::relationContainsInput<eos::PressureFromDensity, eos::quantity::Density>);
STATIC_CHECK_FALSE(eos::relationContainsInput<eos::PressureFromDensity, eos::quantity::Pressure>);
STATIC_CHECK(std::same_as<eos::RelationOutputT<eos::PressureFromDensity>, eos::quantity::Pressure>);
STATIC_CHECK(std::same_as<eos::RelationInputT<0, eos::PressureFromDensity>, eos::quantity::Density>);
}
TEST_CASE(
"EOS Model Contract Requires Every Declared Relation",
tags::equation_of_state_relation_contract
) {
STATIC_CHECK(eos::EquationOfStateModel<CompleteEquationOfState>);
STATIC_CHECK_FALSE(eos::EquationOfStateModel<MissingRelationImplementation>);
STATIC_CHECK_FALSE(eos::EquationOfStateModel<IncorrectRelationOutput>);
STATIC_CHECK_FALSE(eos::EquationOfStateModel<InvalidRelationCatalog>);
STATIC_CHECK(eos::SupportsRelation<CompleteEquationOfState, eos::PressureFromDensity>);
STATIC_CHECK_FALSE(eos::SupportsRelation<CompleteEquationOfState, eos::DensityFromSpecificEnthalpy>);
STATIC_CHECK_FALSE(CanEvaluateDensityFromSpecificEnthalpy<CompleteEquationOfState>);
STATIC_CHECK(
eos::SupportsPartialDerivative<CompleteEquationOfState, SpecificEnthalpyFromPressureAndEntropy, Entropy>
);
STATIC_CHECK_FALSE(
eos::SupportsPartialDerivative<
CompleteEquationOfState, SpecificEnthalpyFromPressureAndEntropy, ElectronFraction>
);
}
TEST_CASE(
"EOS Evaluation Selects Relations From Typed Inputs",
tags::equation_of_state_relation_contract
) {
constexpr CompleteEquationOfState equationOfState;
constexpr eos::PressureValue pressure =
eos::evaluate<eos::quantity::Pressure>(equationOfState, eos::DensityValue{1.25});
constexpr eos::SpecificEnthalpyValue specificEnthalpy = eos::evaluate<eos::quantity::SpecificEnthalpy>(
equationOfState, eos::PressureValue{0.5}, eos::QuantityValue<Entropy>{0.2}
);
constexpr auto entropyDerivative = eos::partialDerivative<eos::quantity::SpecificEnthalpy, Entropy>(
equationOfState, eos::PressureValue{0.5}, eos::QuantityValue<Entropy>{0.2}
);
STATIC_CHECK(noexcept(eos::evaluate<eos::quantity::Pressure>(equationOfState, eos::DensityValue{1.25})));
STATIC_CHECK(
noexcept(eos::partialDerivative<eos::quantity::SpecificEnthalpy, Entropy>(
equationOfState, eos::PressureValue{0.5}, eos::QuantityValue<Entropy>{0.2}
))
);
STATIC_CHECK(pressure.value() == 2.5);
STATIC_CHECK(specificEnthalpy.value() == 2.5);
STATIC_CHECK(entropyDerivative.value() == 5.0);
}
TEST_CASE(
"EOS Evaluation Errors Retain A Structured Cause",
tags::equation_of_state_relation_contract
) {
const eos::EvaluationError error(
eos::EvaluationErrorCode::outside_domain, "Density is outside the relation domain."
);
CHECK(error.code() == eos::EvaluationErrorCode::outside_domain);
CHECK(std::string_view{error.what()} == "Density is outside the relation domain.");
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,156 @@
#include <array>
#include <cmath>
#include <limits>
#include <stdexcept>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
import mean_field;
import test_helpers;
namespace eos = mean_field::eos;
TEST_CASE(
"Polytropic EOS Pressure To Specific Enthalpy Relation Is Characterized",
tags::polytropic_eos_characterization
) {
constexpr std::array<double, 3> polytropicIndices{1.0, 1.5, 3.0};
constexpr std::array<double, 3> polytropicConstants{0.25, 0.73, 2.0};
constexpr std::array<double, 5> pressures{0.0, 1.0e-12, 1.0e-4, 0.3, 5.0};
for (const double polytropicIndex : polytropicIndices) {
for (const double polytropicConstant : polytropicConstants) {
const mean_field::eos::Polytrope equationOfState(polytropicIndex, polytropicConstant);
for (const double pressure : pressures) {
CAPTURE(polytropicIndex, polytropicConstant, pressure);
const double indexPlusOne = polytropicIndex + 1.0;
const double expectedEnthalpy = indexPlusOne *
std::pow(polytropicConstant, polytropicIndex / indexPlusOne) *
std::pow(pressure, 1.0 / indexPlusOne);
const double enthalpy =
eos::evaluate<eos::quantity::SpecificEnthalpy>(equationOfState, eos::PressureValue{pressure})
.value();
if (pressure == 0.0) {
CHECK(enthalpy == 0.0);
} else {
CHECK_THAT(enthalpy, Catch::Matchers::WithinRel(expectedEnthalpy, 5.0e-14));
const double recoveredPressure =
eos::evaluate<eos::quantity::Pressure>(equationOfState, eos::SpecificEnthalpyValue{enthalpy})
.value();
CHECK_THAT(recoveredPressure, Catch::Matchers::WithinRel(pressure, 5.0e-13));
}
}
}
}
}
TEST_CASE(
"Polytropic EOS Domain Contract Covers Every Relation",
tags::polytropic_eos_characterization
) {
constexpr double infinity = std::numeric_limits<double>::infinity();
constexpr double quietNaN = std::numeric_limits<double>::quiet_NaN();
for (const double invalidIndex : std::array<double, 4>{0.999, infinity, -infinity, quietNaN}) {
CAPTURE(invalidIndex);
CHECK_THROWS_AS(mean_field::eos::Polytrope(invalidIndex, 1.0), std::invalid_argument);
}
for (const double invalidConstant : std::array<double, 5>{0.0, -0.1, infinity, -infinity, quietNaN}) {
CAPTURE(invalidConstant);
CHECK_THROWS_AS(mean_field::eos::Polytrope(3.0, invalidConstant), std::invalid_argument);
}
const mean_field::eos::Polytrope equationOfState(3.0, 0.75);
constexpr double negativeDensity = -0.1;
CHECK_THROWS_AS(
eos::evaluate<eos::quantity::Pressure>(equationOfState, eos::DensityValue{negativeDensity}), std::domain_error
);
CHECK_THROWS_AS(
eos::evaluate<eos::quantity::SpecificEnthalpy>(equationOfState, eos::DensityValue{negativeDensity}),
std::domain_error
);
CHECK_THROWS_AS(
(eos::partialDerivative<eos::quantity::Pressure, eos::quantity::Density>(
equationOfState, eos::DensityValue{negativeDensity}
)),
std::domain_error
);
CHECK_THROWS_AS(
eos::evaluate<eos::quantity::SpecificEnthalpy>(equationOfState, eos::PressureValue{-0.1}), std::domain_error
);
constexpr double exteriorEnthalpy = -0.1;
CHECK(
eos::evaluate<eos::quantity::Density>(equationOfState, eos::SpecificEnthalpyValue{exteriorEnthalpy}).value() ==
0.0
);
CHECK(
eos::evaluate<eos::quantity::Pressure>(equationOfState, eos::SpecificEnthalpyValue{exteriorEnthalpy}).value() ==
0.0
);
CHECK(
(eos::partialDerivative<eos::quantity::Density, eos::quantity::SpecificEnthalpy>(
equationOfState, eos::SpecificEnthalpyValue{exteriorEnthalpy}
)
.value() == 0.0)
);
CHECK(
(eos::partialDerivative<eos::quantity::Pressure, eos::quantity::SpecificEnthalpy>(
equationOfState, eos::SpecificEnthalpyValue{exteriorEnthalpy}
)
.value() == 0.0)
);
for (const double nonfiniteValue : std::array<double, 3>{infinity, -infinity, quietNaN}) {
CAPTURE(nonfiniteValue);
CHECK_THROWS_AS(
eos::evaluate<eos::quantity::Pressure>(equationOfState, eos::DensityValue{nonfiniteValue}),
std::domain_error
);
CHECK_THROWS_AS(
eos::evaluate<eos::quantity::SpecificEnthalpy>(equationOfState, eos::DensityValue{nonfiniteValue}),
std::domain_error
);
CHECK_THROWS_AS(
(eos::partialDerivative<eos::quantity::Pressure, eos::quantity::Density>(
equationOfState, eos::DensityValue{nonfiniteValue}
)),
std::domain_error
);
CHECK_THROWS_AS(
eos::evaluate<eos::quantity::SpecificEnthalpy>(equationOfState, eos::PressureValue{nonfiniteValue}),
std::domain_error
);
CHECK_THROWS_AS(
eos::evaluate<eos::quantity::Density>(equationOfState, eos::SpecificEnthalpyValue{nonfiniteValue}),
std::domain_error
);
CHECK_THROWS_AS(
eos::evaluate<eos::quantity::Pressure>(equationOfState, eos::SpecificEnthalpyValue{nonfiniteValue}),
std::domain_error
);
CHECK_THROWS_AS(
(eos::partialDerivative<eos::quantity::Density, eos::quantity::SpecificEnthalpy>(
equationOfState, eos::SpecificEnthalpyValue{nonfiniteValue}
)),
std::domain_error
);
CHECK_THROWS_AS(
(eos::partialDerivative<eos::quantity::Pressure, eos::quantity::SpecificEnthalpy>(
equationOfState, eos::SpecificEnthalpyValue{nonfiniteValue}
)),
std::domain_error
);
}
}

View File

@@ -0,0 +1,200 @@
#include <array>
#include <cmath>
#include <concepts>
#include <limits>
#include <type_traits>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
import mean_field;
import test_helpers;
namespace {
namespace eos = mean_field::eos;
template <typename Candidate>
concept HasAnyUnaryEquationOfStateConversion =
requires(const Candidate &candidate, const double value) { candidate.pressure_from_density(value); } ||
requires(const Candidate &candidate, const double value) { candidate.pressure_from_enthalpy(value); } ||
requires(const Candidate &candidate, const double value) { candidate.enthalpy_from_density(value); } ||
requires(const Candidate &candidate, const double value) { candidate.enthalpy_from_pressure(value); } ||
requires(const Candidate &candidate, const double value) { candidate.density_from_enthalpy(value); } ||
requires(const Candidate &candidate, const double value) {
candidate.density_derivative_from_enthalpy(value);
} ||
requires(const Candidate &candidate, const double value) {
candidate.pressure_derivative_from_enthalpy(value);
} ||
requires(const Candidate &candidate, const double value) { candidate.pressure_derivative_from_density(value); };
} // namespace
TEST_CASE(
"Polytropic EOS Declares Its Thermodynamic Relation Contract",
tags::polytropic_eos_relation_contract
) {
using Polytrope = eos::Polytrope;
STATIC_CHECK(eos::EquationOfStateModel<Polytrope>);
STATIC_CHECK_FALSE(std::is_polymorphic_v<Polytrope>);
STATIC_CHECK_FALSE(HasAnyUnaryEquationOfStateConversion<Polytrope>);
STATIC_CHECK(Polytrope::Relations::size == 5);
STATIC_CHECK(eos::SupportsRelation<Polytrope, eos::PressureFromDensity>);
STATIC_CHECK(eos::SupportsRelation<Polytrope, eos::PressureFromSpecificEnthalpy>);
STATIC_CHECK(eos::SupportsRelation<Polytrope, eos::SpecificEnthalpyFromDensity>);
STATIC_CHECK(eos::SupportsRelation<Polytrope, eos::SpecificEnthalpyFromPressure>);
STATIC_CHECK(eos::SupportsRelation<Polytrope, eos::DensityFromSpecificEnthalpy>);
STATIC_CHECK(eos::SupportsPartialDerivative<Polytrope, eos::PressureFromDensity, eos::quantity::Density>);
STATIC_CHECK(
eos::SupportsPartialDerivative<Polytrope, eos::PressureFromSpecificEnthalpy, eos::quantity::SpecificEnthalpy>
);
STATIC_CHECK(
eos::SupportsPartialDerivative<Polytrope, eos::DensityFromSpecificEnthalpy, eos::quantity::SpecificEnthalpy>
);
STATIC_CHECK_FALSE(
eos::SupportsPartialDerivative<Polytrope, eos::SpecificEnthalpyFromPressure, eos::quantity::Pressure>
);
STATIC_CHECK_FALSE(
eos::SupportsPartialDerivative<Polytrope, eos::SpecificEnthalpyFromDensity, eos::quantity::Density>
);
}
TEST_CASE(
"Polytropic EOS Typed Relations Preserve Analytic Values",
tags::polytropic_eos_characterization
) {
constexpr std::array<double, 3> polytropicIndices{1.0, 1.5, 3.0};
constexpr std::array<double, 2> polytropicConstants{0.25, 0.73};
constexpr std::array<double, 4> densities{0.0, 1.0e-6, 0.2, 2.0};
constexpr std::array<double, 4> specificEnthalpies{-0.3, 0.0, 0.2, 1.7};
constexpr std::array<double, 4> pressures{0.0, 1.0e-8, 0.3, 4.0};
for (const double polytropicIndex : polytropicIndices) {
for (const double polytropicConstant : polytropicConstants) {
const eos::Polytrope equationOfState(polytropicIndex, polytropicConstant);
for (const double density : densities) {
CAPTURE(polytropicIndex, polytropicConstant, density);
const double expectedPressure = polytropicConstant * std::pow(density, 1.0 + 1.0 / polytropicIndex);
const double expectedSpecificEnthalpy =
(polytropicIndex + 1.0) * polytropicConstant * std::pow(density, 1.0 / polytropicIndex);
CHECK(
eos::evaluate<eos::quantity::Pressure>(equationOfState, eos::DensityValue{density}).value() ==
expectedPressure
);
CHECK(
eos::evaluate<eos::quantity::SpecificEnthalpy>(equationOfState, eos::DensityValue{density})
.value() == expectedSpecificEnthalpy
);
CHECK_THAT(
(eos::partialDerivative<eos::quantity::Pressure, eos::quantity::Density>(
equationOfState, eos::DensityValue{density}
)
.value()),
Catch::Matchers::WithinRel(
density == 0.0 ? 0.0 : expectedSpecificEnthalpy / polytropicIndex, 2.0e-15
)
);
}
for (const double specificEnthalpy : specificEnthalpies) {
CAPTURE(polytropicIndex, polytropicConstant, specificEnthalpy);
const double expectedDensity =
specificEnthalpy <= 0.0
? 0.0
: std::pow(specificEnthalpy / ((polytropicIndex + 1.0) * polytropicConstant), polytropicIndex);
CHECK(
eos::evaluate<eos::quantity::Density>(equationOfState, eos::SpecificEnthalpyValue{specificEnthalpy})
.value() == expectedDensity
);
CHECK(
eos::evaluate<eos::quantity::Pressure>(
equationOfState, eos::SpecificEnthalpyValue{specificEnthalpy}
)
.value() ==
(specificEnthalpy <= 0.0 ? 0.0 : expectedDensity * specificEnthalpy / (polytropicIndex + 1.0))
);
CHECK(
(eos::partialDerivative<eos::quantity::Pressure, eos::quantity::SpecificEnthalpy>(
equationOfState, eos::SpecificEnthalpyValue{specificEnthalpy}
)
.value() == expectedDensity)
);
}
for (const double pressure : pressures) {
CAPTURE(polytropicIndex, polytropicConstant, pressure);
const double indexPlusOne = polytropicIndex + 1.0;
const double expectedSpecificEnthalpy = indexPlusOne *
std::pow(polytropicConstant, polytropicIndex / indexPlusOne) *
std::pow(pressure, 1.0 / indexPlusOne);
CHECK(
eos::evaluate<eos::quantity::SpecificEnthalpy>(equationOfState, eos::PressureValue{pressure})
.value() == expectedSpecificEnthalpy
);
}
}
}
}
TEST_CASE(
"Typed Polytropic EOS Preserves Domain And Exterior Semantics",
tags::polytropic_eos_relation_contract
) {
const eos::Polytrope equationOfState(3.0, 0.75);
try {
static_cast<void>(eos::evaluate<eos::quantity::Pressure>(equationOfState, eos::DensityValue{-0.1}));
FAIL("A negative density must be rejected.");
} catch (const eos::EvaluationError &error) {
CHECK(error.code() == eos::EvaluationErrorCode::outside_domain);
}
try {
static_cast<void>(eos::evaluate<eos::quantity::SpecificEnthalpy>(
equationOfState, eos::PressureValue{std::numeric_limits<double>::quiet_NaN()}
));
FAIL("A nonfinite pressure must be rejected.");
} catch (const eos::EvaluationError &error) {
CHECK(error.code() == eos::EvaluationErrorCode::nonfinite_input);
}
constexpr double exteriorSpecificEnthalpy = -0.3;
CHECK(
eos::evaluate<eos::quantity::Density>(equationOfState, eos::SpecificEnthalpyValue{exteriorSpecificEnthalpy})
.value() == 0.0
);
CHECK(
eos::evaluate<eos::quantity::Pressure>(equationOfState, eos::SpecificEnthalpyValue{exteriorSpecificEnthalpy})
.value() == 0.0
);
CHECK(
(eos::partialDerivative<eos::quantity::Density, eos::quantity::SpecificEnthalpy>(
equationOfState, eos::SpecificEnthalpyValue{exteriorSpecificEnthalpy}
)
.value() == 0.0)
);
CHECK(
(eos::partialDerivative<eos::quantity::Pressure, eos::quantity::SpecificEnthalpy>(
equationOfState, eos::SpecificEnthalpyValue{exteriorSpecificEnthalpy}
)
.value() == 0.0)
);
}