feat(libmeanfield): variadic refactor

also added normaliztion operator
This commit is contained in:
2026-09-06 10:15:00 -04:00
parent 71423d543f
commit 76818f2f82
63 changed files with 28794 additions and 1119 deletions

View File

@@ -0,0 +1,50 @@
add_library(mean_field_extension_example)
target_sources(
mean_field_extension_example
PUBLIC
FILE_SET CXX_MODULES FILES
ideal_gas_radiation.cppm
rotating_stellar_model.cppm
)
target_link_libraries(mean_field_extension_example PUBLIC mean_field)
add_executable(extension_example_demo demo.cpp)
target_link_libraries(extension_example_demo PRIVATE mean_field_extension_example)
add_executable(
extension_example_tests
tests/ideal_gas_radiation.cpp
tests/rotating_stellar_model.cpp
)
target_link_libraries(
extension_example_tests
PRIVATE
mean_field_extension_example
Catch2::Catch2WithMain
)
catch_discover_tests(
extension_example_tests
TEST_PREFIX "extension_example::"
PROPERTIES LABELS "extension-example"
)
find_program(LATEXMK_EXECUTABLE latexmk)
if (LATEXMK_EXECUTABLE)
add_custom_target(
extension_example_manual
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/manual"
COMMAND
${LATEXMK_EXECUTABLE}
-pdf
-interaction=nonstopmode
-halt-on-error
-outdir=${CMAKE_CURRENT_BINARY_DIR}/manual
"${CMAKE_CURRENT_SOURCE_DIR}/manual/physics_developer_manual.tex"
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/manual"
COMMENT "Compiling the MeanField physics developer manual"
VERBATIM
)
endif ()

View File

@@ -0,0 +1,55 @@
# MeanField physics extension example
This directory is a small, isolated example for physicists who want to extend
MeanField without first learning its internal block-matrix machinery.
Start in this order:
1. Read `ideal_gas_radiation.cppm`. It implements a monatomic ideal gas plus
equilibrium radiation using the public EOS relation protocol.
2. Read `rotating_stellar_model.cppm`. It composes that EOS with the existing
isobaric surface, fixed-total-mass invariant, and fixed-angular-momentum
invariant.
3. Read and run `demo.cpp`.
4. Read the tests. They show which claims should be compile-time contracts and
which claims require physical or numerical checks.
5. Use `manual/physics_developer_manual.pdf` as the detailed guide. Its LaTeX
source is beside it.
## The important boundary
`makeRotatingStellarModel(...)` produces a valid, strongly typed stellar-model
specification. The current equilibrium numerical core is still barotropic: it
expects density to be closed by specific enthalpy alone. An ideal-gas plus
radiation EOS depends independently on density and temperature, so a complete
thermal equilibrium solve also needs a temperature or entropy field and its
governing equation.
The example therefore proves at compile time that model composition succeeds
and that the present discretizer rejects this model. It does not disguise the
thermal EOS as a polytrope or claim that a missing energy equation exists.
## Build only this example
From the repository root, configure as usual, then build only these targets:
```sh
cmake --build cmake-build-profile-homebrew-llvm \
--target extension_example_demo extension_example_tests
```
Run only the extension tests:
```sh
./cmake-build-profile-homebrew-llvm/extension_example/extension_example_tests
```
Compile a fresh manual into the build directory:
```sh
cmake --build cmake-build-profile-homebrew-llvm \
--target extension_example_manual
```
No source under `libmeanfield/` belongs to this example, and the extension test
executable is separate from the main MeanField regression suite.

View File

@@ -0,0 +1,43 @@
#include <iomanip>
#include <iostream>
import mean_field;
import mean_field_extension_example.rotating_stellar_model;
int main() {
using namespace mean_field;
using namespace mean_field::extension_example;
const IdealGasRadiation equationOfState({
.meanMolecularWeight = 0.61,
.boltzmannConstant = 1.380649e-16,
.atomicMassUnit = 1.66053906660e-24,
.radiationConstant = 7.5657e-15
});
const dimensions::DensityValue density{10.0}; // g cm^-3
const dimensions::TemperatureValue temperature{1.5e7}; // K
const auto pressure = eos::evaluate<dimensions::quantity::Pressure>(
equationOfState,
density,
temperature
);
const auto model = makeRotatingStellarModel({
.equationOfState = equationOfState.parameters(),
.surfacePressure = dimensions::PressureValue{0.0},
.totalMass = dimensions::MassValue{1.0},
.totalAngularMomentum = dimensions::AngularMomentumValue{0.2}
});
std::cout << std::scientific
<< "P(rho = 10 g cm^-3, T = 1.5e7 K) = "
<< pressure.value() << " dyn cm^-2\n"
<< "Compiled specification count = "
<< model.specificationCount << '\n'
<< "Current barotropic backend accepts this thermal model = "
<< std::boolalpha
<< currentEquilibriumBackendSupportsIdealGasRadiation << '\n';
return 0;
}

View File

@@ -0,0 +1,403 @@
module;
#include <cmath>
#include <stdexcept>
#include <string>
export module mean_field_extension_example.ideal_gas_radiation;
import mean_field;
/*
* This file is intended to be read from top to bottom by a physicist who is
* adding an equation of state (EOS). The comments explain the small amount
* of type-system vocabulary required by MeanField; the thermodynamics remain
* visible as ordinary equations.
*/
export namespace mean_field::extension_example {
namespace eos_quantity = mean_field::dimensions::quantity;
/*
* A relation is only a compile-time sentence:
*
* output = f(input 1, input 2, ...).
*
* Input order is significant. These declarations say that density is
* the first argument and temperature is the second argument. They do not
* allocate data and have no runtime cost.
*/
using PressureFromDensityAndTemperature = mean_field::eos::Relation<
eos_quantity::Pressure,
eos_quantity::Density,
eos_quantity::Temperature>;
using SpecificInternalEnergyFromDensityAndTemperature = mean_field::eos::Relation<
eos_quantity::SpecificInternalEnergy,
eos_quantity::Density,
eos_quantity::Temperature>;
using SpecificEnthalpyFromDensityAndTemperature = mean_field::eos::Relation<
eos_quantity::SpecificEnthalpy,
eos_quantity::Density,
eos_quantity::Temperature>;
/*
* A monatomic ideal gas plus equilibrium radiation:
*
* R = k_B / (mu m_u)
* P_gas = rho R T
* P_rad = a T^4 / 3
* u = (3/2) R T + a T^4 / rho
* h = u + P/rho
* = (5/2) R T + 4 a T^4 / (3 rho)
*
* The scalar QuantityValue wrappers identify what a number means. They
* intentionally do not perform unit conversion. Every number supplied
* here must therefore use one coherent unit system.
*/
class IdealGasRadiation final {
public:
struct Parameters final {
/* Mean particle mass in atomic-mass units. */
double meanMolecularWeight{0.61};
/* CGS defaults: erg K^-1, g, and erg cm^-3 K^-4. */
double boltzmannConstant{1.380649e-16};
double atomicMassUnit{1.66053906660e-24};
double radiationConstant{7.5657e-15};
};
/*
* This one alias makes the EOS a constitutive-law specification that
* can be placed directly in model::StellarModel(...). There is no
* registry edit and no central list of EOS combinations to maintain.
*/
using ModelDefinition = mean_field::eos::ConstitutiveLaw<IdealGasRadiation,"IdealGasRadiation">;
/*
* The catalog is the complete public claim made by this EOS. If an
* evaluate overload below is missing or has the wrong argument order,
* eos::EquationOfStateModel<IdealGasRadiation> becomes false at
* compile time.
*/
using Relations = mean_field::eos::RelationCatalog<
PressureFromDensityAndTemperature,
SpecificInternalEnergyFromDensityAndTemperature,
SpecificEnthalpyFromDensityAndTemperature
>;
struct PressureContributions final {
mean_field::dimensions::PressureValue gas;
mean_field::dimensions::PressureValue radiation;
[[nodiscard]] mean_field::dimensions::PressureValue total() const noexcept {
return gas + radiation;
}
};
explicit IdealGasRadiation(const Parameters parameters)
: m_parameters(validatedParameters(parameters)),
m_specificGasConstant(
m_parameters.boltzmannConstant /(m_parameters.meanMolecularWeight * m_parameters.atomicMassUnit)
) {}
[[nodiscard]] const Parameters &parameters() const noexcept {
return m_parameters;
}
[[nodiscard]] double specificGasConstant() const noexcept {
return m_specificGasConstant;
}
/*
* Named component functions are not required by the EOS protocol.
* They are provided because they make diagnostics and physics tests
* easier to read than repeated algebra in client code.
*/
[[nodiscard]] PressureContributions pressureContributions(
const mean_field::dimensions::DensityValue density,
const mean_field::dimensions::TemperatureValue temperature
) const {
validateMaterialState(density, temperature);
const double rho = density.value();
const double T = temperature.value();
return PressureContributions{
.gas = mean_field::dimensions::PressureValue{rho * m_specificGasConstant * T},
.radiation = mean_field::dimensions::PressureValue{
m_parameters.radiationConstant * fourthPower(T) / 3.0
}
};
}
[[nodiscard]] mean_field::dimensions::SpecificInternalEnergyValue gasSpecificInternalEnergy(
const mean_field::dimensions::TemperatureValue temperature
) const {
validateTemperature(temperature);
return mean_field::dimensions::SpecificInternalEnergyValue{
1.5 * m_specificGasConstant * temperature.value()
};
}
[[nodiscard]] mean_field::dimensions::SpecificInternalEnergyValue radiationSpecificInternalEnergy(
const mean_field::dimensions::DensityValue density,
const mean_field::dimensions::TemperatureValue temperature
) const {
validateMaterialState(density, temperature);
return mean_field::dimensions::SpecificInternalEnergyValue{
m_parameters.radiationConstant * fourthPower(temperature.value()) / density.value()
};
}
/* The evaluate overloads implement the three declared relations. */
[[nodiscard]] mean_field::dimensions::PressureValue evaluate(
PressureFromDensityAndTemperature,
const mean_field::dimensions::DensityValue density,
const mean_field::dimensions::TemperatureValue temperature
) const {
return pressureContributions(density, temperature).total();
}
[[nodiscard]] mean_field::dimensions::SpecificInternalEnergyValue evaluate(
SpecificInternalEnergyFromDensityAndTemperature,
const mean_field::dimensions::DensityValue density,
const mean_field::dimensions::TemperatureValue temperature
) const {
const auto gas = gasSpecificInternalEnergy(temperature);
const auto radiation = radiationSpecificInternalEnergy(density, temperature);
return gas + radiation;
}
[[nodiscard]] mean_field::dimensions::SpecificEnthalpyValue evaluate(
SpecificEnthalpyFromDensityAndTemperature,
const mean_field::dimensions::DensityValue density,
const mean_field::dimensions::TemperatureValue temperature
) const {
validateMaterialState(density, temperature);
const double rho = density.value();
const double T = temperature.value();
return mean_field::dimensions::SpecificEnthalpyValue{
2.5 * m_specificGasConstant * T +
4.0 * m_parameters.radiationConstant * fourthPower(T) / (3.0 * rho)
};
}
/*
* Jacobian entries are ordinary analytic partial derivatives. The
* WithRespectTo tag prevents accidentally returning dP/dT from the
* overload that promised dP/drho.
*/
[[nodiscard]] mean_field::eos::PartialDerivative<
eos_quantity::Pressure,
eos_quantity::Density>
partialDerivative(
PressureFromDensityAndTemperature,
mean_field::eos::WithRespectTo<eos_quantity::Density>,
const mean_field::dimensions::DensityValue density,
const mean_field::dimensions::TemperatureValue temperature
) const {
validateMaterialState(density, temperature);
return mean_field::eos::PartialDerivative<
eos_quantity::Pressure,
eos_quantity::Density>{m_specificGasConstant * temperature.value()};
}
[[nodiscard]] mean_field::eos::PartialDerivative<
eos_quantity::Pressure,
eos_quantity::Temperature>
partialDerivative(
PressureFromDensityAndTemperature,
mean_field::eos::WithRespectTo<eos_quantity::Temperature>,
const mean_field::dimensions::DensityValue density,
const mean_field::dimensions::TemperatureValue temperature
) const {
validateMaterialState(density, temperature);
const double T = temperature.value();
return mean_field::eos::PartialDerivative<
eos_quantity::Pressure,
eos_quantity::Temperature>{
density.value() * m_specificGasConstant +
4.0 * m_parameters.radiationConstant * cube(T) / 3.0
};
}
[[nodiscard]] mean_field::eos::PartialDerivative<
eos_quantity::SpecificInternalEnergy,
eos_quantity::Density>
partialDerivative(
SpecificInternalEnergyFromDensityAndTemperature,
mean_field::eos::WithRespectTo<eos_quantity::Density>,
const mean_field::dimensions::DensityValue density,
const mean_field::dimensions::TemperatureValue temperature
) const {
validateMaterialState(density, temperature);
return mean_field::eos::PartialDerivative<
eos_quantity::SpecificInternalEnergy,
eos_quantity::Density>{
-m_parameters.radiationConstant * fourthPower(temperature.value()) /
square(density.value())
};
}
[[nodiscard]] mean_field::eos::PartialDerivative<
eos_quantity::SpecificInternalEnergy,
eos_quantity::Temperature>
partialDerivative(
SpecificInternalEnergyFromDensityAndTemperature,
mean_field::eos::WithRespectTo<eos_quantity::Temperature>,
const mean_field::dimensions::DensityValue density,
const mean_field::dimensions::TemperatureValue temperature
) const {
validateMaterialState(density, temperature);
return mean_field::eos::PartialDerivative<
eos_quantity::SpecificInternalEnergy,
eos_quantity::Temperature>{
1.5 * m_specificGasConstant +
4.0 * m_parameters.radiationConstant * cube(temperature.value()) / density.value()
};
}
[[nodiscard]] mean_field::eos::PartialDerivative<
eos_quantity::SpecificEnthalpy,
eos_quantity::Density>
partialDerivative(
SpecificEnthalpyFromDensityAndTemperature,
mean_field::eos::WithRespectTo<eos_quantity::Density>,
const mean_field::dimensions::DensityValue density,
const mean_field::dimensions::TemperatureValue temperature
) const {
validateMaterialState(density, temperature);
return mean_field::eos::PartialDerivative<
eos_quantity::SpecificEnthalpy,
eos_quantity::Density>{
-4.0 * m_parameters.radiationConstant * fourthPower(temperature.value()) /
(3.0 * square(density.value()))
};
}
[[nodiscard]] mean_field::eos::PartialDerivative<
eos_quantity::SpecificEnthalpy,
eos_quantity::Temperature>
partialDerivative(
SpecificEnthalpyFromDensityAndTemperature,
mean_field::eos::WithRespectTo<eos_quantity::Temperature>,
const mean_field::dimensions::DensityValue density,
const mean_field::dimensions::TemperatureValue temperature
) const {
validateMaterialState(density, temperature);
return mean_field::eos::PartialDerivative<
eos_quantity::SpecificEnthalpy,
eos_quantity::Temperature>{
2.5 * m_specificGasConstant +
16.0 * m_parameters.radiationConstant * cube(temperature.value()) /
(3.0 * density.value())
};
}
private:
[[nodiscard]] static Parameters validatedParameters(const Parameters parameters) {
requirePositiveFinite(parameters.meanMolecularWeight, "mean molecular weight");
requirePositiveFinite(parameters.boltzmannConstant, "Boltzmann constant");
requirePositiveFinite(parameters.atomicMassUnit, "atomic mass unit");
requireNonnegativeFinite(parameters.radiationConstant, "radiation constant");
return parameters;
}
static void requirePositiveFinite(const double value, const char *name) {
if (!std::isfinite(value) || value <= 0.0) {
throw std::invalid_argument(
std::string{"IdealGasRadiation requires a finite, positive "} + name + "."
);
}
}
static void requireNonnegativeFinite(const double value, const char *name) {
if (!std::isfinite(value) || value < 0.0) {
throw std::invalid_argument(
std::string{"IdealGasRadiation requires a finite, nonnegative "} + name + "."
);
}
}
static void validateMaterialState(
const mean_field::dimensions::DensityValue density,
const mean_field::dimensions::TemperatureValue temperature
) {
if (!std::isfinite(density.value()) || !std::isfinite(temperature.value())) {
throw mean_field::eos::EvaluationError{
mean_field::eos::EvaluationErrorCode::nonfinite_input,
"IdealGasRadiation requires finite density and temperature."
};
}
if (density.value() <= 0.0 || temperature.value() < 0.0) {
throw mean_field::eos::EvaluationError{
mean_field::eos::EvaluationErrorCode::outside_domain,
"IdealGasRadiation requires rho > 0 and T >= 0."
};
}
}
static void validateTemperature(const mean_field::dimensions::TemperatureValue temperature) {
if (!std::isfinite(temperature.value())) {
throw mean_field::eos::EvaluationError{
mean_field::eos::EvaluationErrorCode::nonfinite_input,
"IdealGasRadiation requires finite temperature."
};
}
if (temperature.value() < 0.0) {
throw mean_field::eos::EvaluationError{
mean_field::eos::EvaluationErrorCode::outside_domain,
"IdealGasRadiation requires T >= 0."
};
}
}
[[nodiscard]] static double square(const double value) noexcept {
return value * value;
}
[[nodiscard]] static double cube(const double value) noexcept {
return value * value * value;
}
[[nodiscard]] static double fourthPower(const double value) noexcept {
const double squared = square(value);
return squared * squared;
}
Parameters m_parameters;
double m_specificGasConstant;
};
/*
* These assertions are executable documentation. They prove that the
* class and every derivative satisfy the public extension protocol.
*/
static_assert(mean_field::models::SelfDescribingModelSpecification<IdealGasRadiation>);
static_assert(mean_field::eos::EquationOfStateModel<IdealGasRadiation>);
static_assert(mean_field::eos::SupportsPartialDerivative<
IdealGasRadiation,
PressureFromDensityAndTemperature,
eos_quantity::Density>);
static_assert(mean_field::eos::SupportsPartialDerivative<
IdealGasRadiation,
PressureFromDensityAndTemperature,
eos_quantity::Temperature>);
static_assert(mean_field::eos::SupportsPartialDerivative<
IdealGasRadiation,
SpecificInternalEnergyFromDensityAndTemperature,
eos_quantity::Density>);
static_assert(mean_field::eos::SupportsPartialDerivative<
IdealGasRadiation,
SpecificInternalEnergyFromDensityAndTemperature,
eos_quantity::Temperature>);
static_assert(mean_field::eos::SupportsPartialDerivative<
IdealGasRadiation,
SpecificEnthalpyFromDensityAndTemperature,
eos_quantity::Density>);
static_assert(mean_field::eos::SupportsPartialDerivative<
IdealGasRadiation,
SpecificEnthalpyFromDensityAndTemperature,
eos_quantity::Temperature>);
} // namespace mean_field::extension_example

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,61 @@
module;
#include <array>
#include <utility>
export module mean_field_extension_example.rotating_stellar_model;
export import mean_field_extension_example.ideal_gas_radiation;
import mean_field;
/*
* This file is the physics-facing composition layer. It contains no block
* matrices, generated residual types, Jacobian indices, or preconditioner
* plumbing. StellarModel infers those structural types from the four
* physical specifications passed to it.
*/
export namespace mean_field::extension_example {
struct RotatingStellarModelParameters final {
IdealGasRadiation::Parameters equationOfState;
mean_field::dimensions::PressureValue surfacePressure;
mean_field::dimensions::MassValue totalMass;
mean_field::dimensions::AngularMomentumValue totalAngularMomentum;
std::array<double, 3> rotationAxis{0.0, 0.0, 1.0};
std::array<double, 3> rotationCenter{0.0, 0.0, 0.0};
};
[[nodiscard]] auto makeRotatingStellarModel(const RotatingStellarModelParameters &parameters) {
return mean_field::model::StellarModel(
IdealGasRadiation(parameters.equationOfState),
mean_field::surface::Isobaric({.Psurf = parameters.surfacePressure}),
mean_field::integral::FixedTotalMass({.Mtotal = parameters.totalMass}),
mean_field::integral::FixedAngularMomentum({
.Jtotal = parameters.totalAngularMomentum,
.axis = parameters.rotationAxis,
.center = parameters.rotationCenter
})
);
}
using RotatingStellarModel = decltype(
makeRotatingStellarModel(std::declval<const RotatingStellarModelParameters &>())
);
static_assert(mean_field::model::StellarModelType<RotatingStellarModel>);
static_assert(RotatingStellarModel::symbolicallySquare);
/*
* Deliberate capability boundary:
*
* The specification above is a valid, strongly typed stellar model. The
* current numerical equilibrium core, however, closes density through a
* barotropic relation rho(h). This EOS instead needs an independent
* temperature or entropy field and its governing equation. Keeping this
* assertion false prevents an example from suggesting that discretize()
* already implements thermal equilibrium when it does not.
*/
inline constexpr bool currentEquilibriumBackendSupportsIdealGasRadiation =
mean_field::equilibrium::StellarEquilibriumModel<RotatingStellarModel>;
static_assert(!currentEquilibriumBackendSupportsIdealGasRadiation);
} // namespace mean_field::extension_example

View File

@@ -0,0 +1,292 @@
#include <algorithm>
#include <array>
#include <cmath>
#include <concepts>
#include <limits>
#include <type_traits>
#include <utility>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
import mean_field;
import mean_field_extension_example.ideal_gas_radiation;
namespace {
namespace dimensions = mean_field::dimensions;
namespace eos = mean_field::eos;
namespace example = mean_field::extension_example;
[[nodiscard]] example::IdealGasRadiation makeSimpleEquationOfState() {
/* R = k_B / (mu m_u) = 12 / (2 * 3) = 2. */
return example::IdealGasRadiation({
.meanMolecularWeight = 2.0,
.boltzmannConstant = 12.0,
.atomicMassUnit = 3.0,
.radiationConstant = 9.0
});
}
template <typename Function>
[[nodiscard]] double centeredDifference(
Function function,
const double point
) {
const double step = std::cbrt(std::numeric_limits<double>::epsilon()) *
std::max(1.0, std::abs(point));
return (function(point + step) - function(point - step)) / (2.0 * step);
}
template <typename EquationOfState>
concept CanEvaluatePressureWithReversedInputs = requires(
const EquationOfState &equationOfState,
const dimensions::TemperatureValue temperature,
const dimensions::DensityValue density
) {
eos::evaluate<dimensions::quantity::Pressure>(equationOfState, temperature, density);
};
} // namespace
TEST_CASE("The extension satisfies the EOS protocol at compile time", "[extension-example][eos][type]") {
using EquationOfState = example::IdealGasRadiation;
STATIC_CHECK(mean_field::models::SelfDescribingModelSpecification<EquationOfState>);
STATIC_CHECK(eos::EquationOfStateModel<EquationOfState>);
STATIC_CHECK(eos::SupportsRelation<EquationOfState, example::PressureFromDensityAndTemperature>);
STATIC_CHECK(eos::SupportsRelation<EquationOfState, example::SpecificInternalEnergyFromDensityAndTemperature>);
STATIC_CHECK(eos::SupportsRelation<EquationOfState, example::SpecificEnthalpyFromDensityAndTemperature>);
STATIC_CHECK_FALSE(eos::BarotropicClosureEquationOfState<EquationOfState>);
STATIC_CHECK_FALSE(CanEvaluatePressureWithReversedInputs<EquationOfState>);
using PressureResult = decltype(eos::evaluate<dimensions::quantity::Pressure>(
std::declval<const EquationOfState &>(),
dimensions::DensityValue{1.0},
dimensions::TemperatureValue{1.0}
));
STATIC_CHECK(std::same_as<PressureResult, dimensions::PressureValue>);
}
TEST_CASE("Gas and radiation terms reproduce the defining thermodynamics", "[extension-example][eos][physics]") {
const auto equationOfState = makeSimpleEquationOfState();
const dimensions::DensityValue density{4.0};
const dimensions::TemperatureValue temperature{2.0};
const auto pressureContributions = equationOfState.pressureContributions(density, temperature);
const auto pressure = eos::evaluate<dimensions::quantity::Pressure>(
equationOfState,
density,
temperature
);
const auto internalEnergy = eos::evaluate<dimensions::quantity::SpecificInternalEnergy>(
equationOfState,
density,
temperature
);
const auto enthalpy = eos::evaluate<dimensions::quantity::SpecificEnthalpy>(
equationOfState,
density,
temperature
);
CHECK(equationOfState.specificGasConstant() == Catch::Approx(2.0));
CHECK(pressureContributions.gas.value() == Catch::Approx(16.0));
CHECK(pressureContributions.radiation.value() == Catch::Approx(48.0));
CHECK(pressure.value() == Catch::Approx(64.0));
CHECK(internalEnergy.value() == Catch::Approx(42.0));
CHECK(enthalpy.value() == Catch::Approx(58.0));
/* This is the thermodynamic identity h = u + P/rho. */
CHECK(enthalpy.value() == Catch::Approx(internalEnergy.value() + pressure.value() / density.value()));
}
TEST_CASE("The gas and photon terms have their expected scaling laws", "[extension-example][eos][physics]") {
const auto equationOfState = makeSimpleEquationOfState();
const dimensions::DensityValue density{3.5};
const dimensions::TemperatureValue temperature{1.25};
const auto baseline = equationOfState.pressureContributions(density, temperature);
const auto doubledDensity = equationOfState.pressureContributions(
dimensions::DensityValue{2.0 * density.value()},
temperature
);
const auto doubledTemperature = equationOfState.pressureContributions(
density,
dimensions::TemperatureValue{2.0 * temperature.value()}
);
CHECK(doubledDensity.gas.value() == Catch::Approx(2.0 * baseline.gas.value()));
CHECK(doubledDensity.radiation.value() == Catch::Approx(baseline.radiation.value()));
CHECK(doubledTemperature.gas.value() == Catch::Approx(2.0 * baseline.gas.value()));
CHECK(doubledTemperature.radiation.value() == Catch::Approx(16.0 * baseline.radiation.value()));
const double crossoverTemperature = std::cbrt(
3.0 * density.value() * equationOfState.specificGasConstant() /
equationOfState.parameters().radiationConstant
);
const auto crossover = equationOfState.pressureContributions(
density,
dimensions::TemperatureValue{crossoverTemperature}
);
CHECK(crossover.gas.value() == Catch::Approx(crossover.radiation.value()).epsilon(2.0e-14));
}
TEST_CASE("All declared Jacobian entries match centered numerical derivatives",
"[extension-example][eos][derivative][numerical]") {
const auto equationOfState = example::IdealGasRadiation({
.meanMolecularWeight = 1.25,
.boltzmannConstant = 2.75,
.atomicMassUnit = 0.8,
.radiationConstant = 0.35
});
struct State final {
double density;
double temperature;
};
const std::array states{
State{.density = 0.4, .temperature = 0.7},
State{.density = 2.0, .temperature = 1.5},
State{.density = 11.0, .temperature = 3.0}
};
for (const State state : states) {
const dimensions::DensityValue density{state.density};
const dimensions::TemperatureValue temperature{state.temperature};
const auto pressureDensity = eos::partialDerivative<
dimensions::quantity::Pressure,
dimensions::quantity::Density>(equationOfState, density, temperature);
const auto pressureTemperature = eos::partialDerivative<
dimensions::quantity::Pressure,
dimensions::quantity::Temperature>(equationOfState, density, temperature);
const auto energyDensity = eos::partialDerivative<
dimensions::quantity::SpecificInternalEnergy,
dimensions::quantity::Density>(equationOfState, density, temperature);
const auto energyTemperature = eos::partialDerivative<
dimensions::quantity::SpecificInternalEnergy,
dimensions::quantity::Temperature>(equationOfState, density, temperature);
const auto enthalpyDensity = eos::partialDerivative<
dimensions::quantity::SpecificEnthalpy,
dimensions::quantity::Density>(equationOfState, density, temperature);
const auto enthalpyTemperature = eos::partialDerivative<
dimensions::quantity::SpecificEnthalpy,
dimensions::quantity::Temperature>(equationOfState, density, temperature);
const double numericalPressureDensity = centeredDifference(
[&](const double rho) {
return eos::evaluate<dimensions::quantity::Pressure>(
equationOfState,
dimensions::DensityValue{rho},
temperature
).value();
},
state.density
);
const double numericalPressureTemperature = centeredDifference(
[&](const double T) {
return eos::evaluate<dimensions::quantity::Pressure>(
equationOfState,
density,
dimensions::TemperatureValue{T}
).value();
},
state.temperature
);
const double numericalEnergyDensity = centeredDifference(
[&](const double rho) {
return eos::evaluate<dimensions::quantity::SpecificInternalEnergy>(
equationOfState,
dimensions::DensityValue{rho},
temperature
).value();
},
state.density
);
const double numericalEnergyTemperature = centeredDifference(
[&](const double T) {
return eos::evaluate<dimensions::quantity::SpecificInternalEnergy>(
equationOfState,
density,
dimensions::TemperatureValue{T}
).value();
},
state.temperature
);
const double numericalEnthalpyDensity = centeredDifference(
[&](const double rho) {
return eos::evaluate<dimensions::quantity::SpecificEnthalpy>(
equationOfState,
dimensions::DensityValue{rho},
temperature
).value();
},
state.density
);
const double numericalEnthalpyTemperature = centeredDifference(
[&](const double T) {
return eos::evaluate<dimensions::quantity::SpecificEnthalpy>(
equationOfState,
density,
dimensions::TemperatureValue{T}
).value();
},
state.temperature
);
constexpr double tolerance = 3.0e-9;
CHECK(pressureDensity.value() == Catch::Approx(numericalPressureDensity).epsilon(tolerance));
CHECK(pressureTemperature.value() == Catch::Approx(numericalPressureTemperature).epsilon(tolerance));
CHECK(energyDensity.value() == Catch::Approx(numericalEnergyDensity).epsilon(tolerance));
CHECK(energyTemperature.value() == Catch::Approx(numericalEnergyTemperature).epsilon(tolerance));
CHECK(enthalpyDensity.value() == Catch::Approx(numericalEnthalpyDensity).epsilon(tolerance));
CHECK(enthalpyTemperature.value() == Catch::Approx(numericalEnthalpyTemperature).epsilon(tolerance));
}
}
TEST_CASE("The physical domain is checked at the EOS boundary", "[extension-example][eos][domain]") {
const auto equationOfState = makeSimpleEquationOfState();
const double nan = std::numeric_limits<double>::quiet_NaN();
CHECK_THROWS_AS(
example::IdealGasRadiation({
.meanMolecularWeight = 0.0,
.boltzmannConstant = 1.0,
.atomicMassUnit = 1.0,
.radiationConstant = 1.0
}),
std::invalid_argument
);
CHECK_THROWS_AS(
example::IdealGasRadiation({
.meanMolecularWeight = 1.0,
.boltzmannConstant = 1.0,
.atomicMassUnit = 1.0,
.radiationConstant = -1.0
}),
std::invalid_argument
);
CHECK_THROWS_AS(
eos::evaluate<dimensions::quantity::Pressure>(
equationOfState,
dimensions::DensityValue{0.0},
dimensions::TemperatureValue{1.0}
),
eos::EvaluationError
);
CHECK_THROWS_AS(
eos::evaluate<dimensions::quantity::Pressure>(
equationOfState,
dimensions::DensityValue{1.0},
dimensions::TemperatureValue{-1.0}
),
eos::EvaluationError
);
CHECK_THROWS_AS(
eos::evaluate<dimensions::quantity::Pressure>(
equationOfState,
dimensions::DensityValue{nan},
dimensions::TemperatureValue{1.0}
),
eos::EvaluationError
);
}

View File

@@ -0,0 +1,67 @@
#include <concepts>
#include <type_traits>
#include <catch2/catch_test_macros.hpp>
import mean_field;
import mean_field_extension_example.rotating_stellar_model;
TEST_CASE("The example EOS composes with existing stellar specifications",
"[extension-example][model][type]") {
using namespace mean_field;
namespace example = mean_field::extension_example;
const auto stellarModel = example::makeRotatingStellarModel({
.equationOfState = {
.meanMolecularWeight = 0.62,
.boltzmannConstant = 1.380649e-16,
.atomicMassUnit = 1.66053906660e-24,
.radiationConstant = 7.5657e-15
},
.surfacePressure = dimensions::PressureValue{0.0},
.totalMass = dimensions::MassValue{1.75},
.totalAngularMomentum = dimensions::AngularMomentumValue{0.3},
.rotationAxis = {0.0, 0.0, 4.0},
.rotationCenter = {0.1, -0.2, 0.3}
});
using Model = std::remove_cvref_t<decltype(stellarModel)>;
STATIC_CHECK(std::same_as<Model, example::RotatingStellarModel>);
STATIC_CHECK(model::StellarModelType<Model>);
STATIC_CHECK(Model::symbolicallySquare);
STATIC_CHECK(Model::specificationCount == 4);
STATIC_CHECK(std::same_as<model::EquationOfStateType<Model>, example::IdealGasRadiation>);
STATIC_CHECK(Model::template containsSpecification<integral::FixedTotalMass>);
STATIC_CHECK(Model::template containsSpecification<integral::FixedAngularMomentum>);
STATIC_CHECK(Model::template specificationRoleCount<models::SpecificationRole::constitutive_law> == 1);
STATIC_CHECK(Model::template specificationRoleCount<models::SpecificationRole::boundary_condition> == 1);
STATIC_CHECK(Model::template specificationRoleCount<models::SpecificationRole::invariant> == 2);
CHECK(stellarModel.equationOfState().parameters().meanMolecularWeight == 0.62);
CHECK(stellarModel.surfaceCondition().targetPressure() == dimensions::PressureValue{0.0});
CHECK(stellarModel.specification<integral::FixedTotalMass>().targetMass() == dimensions::MassValue{1.75});
const auto &angularMomentum = stellarModel.specification<integral::FixedAngularMomentum>();
CHECK(angularMomentum.targetAngularMomentum() == dimensions::AngularMomentumValue{0.3});
CHECK(angularMomentum.axis()[0] == 0.0);
CHECK(angularMomentum.axis()[1] == 0.0);
CHECK(angularMomentum.axis()[2] == 1.0);
CHECK(angularMomentum.center()[0] == 0.1);
CHECK(angularMomentum.center()[1] == -0.2);
CHECK(angularMomentum.center()[2] == 0.3);
CHECK(stellarModel.runtimeSpecificationDescriptors().size() == 4);
}
TEST_CASE("The example states the current thermal-runtime boundary explicitly",
"[extension-example][model][capability]") {
using Model = mean_field::extension_example::RotatingStellarModel;
/*
* This is not a failure of model composition. It is the intended
* compile-time rejection of a thermal EOS by a currently barotropic
* numerical core. See the manual section 'What compiles today'.
*/
STATIC_CHECK(mean_field::model::StellarModelType<Model>);
STATIC_CHECK_FALSE(mean_field::extension_example::currentEquilibriumBackendSupportsIdealGasRadiation);
STATIC_CHECK_FALSE(mean_field::equilibrium::StellarEquilibriumModel<Model>);
}