feat(libmeanfield): variadic refactor
also added normaliztion operator
This commit is contained in:
50
extension_example/CMakeLists.txt
Normal file
50
extension_example/CMakeLists.txt
Normal 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 ()
|
||||
55
extension_example/README.md
Normal file
55
extension_example/README.md
Normal 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.
|
||||
43
extension_example/demo.cpp
Normal file
43
extension_example/demo.cpp
Normal 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;
|
||||
}
|
||||
403
extension_example/ideal_gas_radiation.cppm
Normal file
403
extension_example/ideal_gas_radiation.cppm
Normal 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 ¶meters() 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
|
||||
BIN
extension_example/manual/physics_developer_manual.pdf
Normal file
BIN
extension_example/manual/physics_developer_manual.pdf
Normal file
Binary file not shown.
1058
extension_example/manual/physics_developer_manual.tex
Normal file
1058
extension_example/manual/physics_developer_manual.tex
Normal file
File diff suppressed because it is too large
Load Diff
61
extension_example/rotating_stellar_model.cppm
Normal file
61
extension_example/rotating_stellar_model.cppm
Normal 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 ¶meters) {
|
||||
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
|
||||
292
extension_example/tests/ideal_gas_radiation.cpp
Normal file
292
extension_example/tests/ideal_gas_radiation.cpp
Normal 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
|
||||
);
|
||||
}
|
||||
67
extension_example/tests/rotating_stellar_model.cpp
Normal file
67
extension_example/tests/rotating_stellar_model.cpp
Normal 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>);
|
||||
}
|
||||
Reference in New Issue
Block a user