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

@@ -103,7 +103,7 @@ target_sources(mean_field
libmeanfield/impl/solver/preconditioning_diagnostics.cpp libmeanfield/impl/solver/preconditioning_diagnostics.cpp
libmeanfield/impl/preconditioning/gravity_field.cpp libmeanfield/impl/preconditioning/gravity_field.cpp
libmeanfield/impl/operators/prepared_mass_normalization.cpp libmeanfield/impl/operators/prepared_mass_normalization.cpp
libmeanfield/impl/operators/prepared_central_density_stellar_equilibrium.cpp libmeanfield/impl/operators/prepared_angular_momentum.cpp
libmeanfield/impl/operators/prepared_stellar_equilibrium.cpp libmeanfield/impl/operators/prepared_stellar_equilibrium.cpp
) )
@@ -156,6 +156,11 @@ target_sources(mean_field
libmeanfield/interface/preconditioning/specification_border.cppm libmeanfield/interface/preconditioning/specification_border.cppm
libmeanfield/interface/preconditioning/equilibrium_coordinates.cppm libmeanfield/interface/preconditioning/equilibrium_coordinates.cppm
libmeanfield/interface/preconditioning/preconditioning.cppm libmeanfield/interface/preconditioning/preconditioning.cppm
libmeanfield/interface/normalization/plan.cppm
libmeanfield/interface/normalization/physical_riesz.cppm
libmeanfield/interface/normalization/operators.cppm
libmeanfield/interface/normalization/stellar_equilibrium.cppm
libmeanfield/interface/normalization/normalization.cppm
libmeanfield/interface/operators/gravity_field.cppm libmeanfield/interface/operators/gravity_field.cppm
libmeanfield/interface/operators/gravity_field_jacobian.cppm libmeanfield/interface/operators/gravity_field_jacobian.cppm
libmeanfield/interface/operators/kernels/gravity_kernels.cppm libmeanfield/interface/operators/kernels/gravity_kernels.cppm
@@ -197,6 +202,7 @@ target_sources(mean_field
libmeanfield/interface/models/specifications.cppm libmeanfield/interface/models/specifications.cppm
libmeanfield/interface/models/typed_stellar_model.cppm libmeanfield/interface/models/typed_stellar_model.cppm
libmeanfield/interface/models/compiled_fixed_mass.cppm libmeanfield/interface/models/compiled_fixed_mass.cppm
libmeanfield/interface/models/compiled_fixed_angular_momentum.cppm
libmeanfield/interface/models/compiled_fixed_central_density.cppm libmeanfield/interface/models/compiled_fixed_central_density.cppm
libmeanfield/interface/surface/constant.cppm libmeanfield/interface/surface/constant.cppm
libmeanfield/interface/surface/dependencies.cppm libmeanfield/interface/surface/dependencies.cppm
@@ -214,11 +220,13 @@ target_sources(mean_field
libmeanfield/interface/operators/root_manifest.cppm libmeanfield/interface/operators/root_manifest.cppm
libmeanfield/interface/operators/prepared_constraint.cppm libmeanfield/interface/operators/prepared_constraint.cppm
libmeanfield/interface/operators/prepared_mass_normalization.cppm libmeanfield/interface/operators/prepared_mass_normalization.cppm
libmeanfield/interface/operators/prepared_angular_momentum.cppm
libmeanfield/interface/operators/prepared_central_density.cppm libmeanfield/interface/operators/prepared_central_density.cppm
libmeanfield/interface/operators/prepared_centering_constraint.cppm libmeanfield/interface/operators/prepared_centering_constraint.cppm
libmeanfield/interface/operators/prepared_surface_constraint.cppm libmeanfield/interface/operators/prepared_surface_constraint.cppm
libmeanfield/interface/operators/prepared_stellar_equilibrium.cppm libmeanfield/interface/operators/prepared_stellar_equilibrium.cppm
libmeanfield/interface/operators/prepared_central_density_stellar_equilibrium.cppm libmeanfield/interface/operators/stellar_equilibrium_compiler.cppm
libmeanfield/interface/operators/prepared_variadic_stellar_equilibrium.cppm
libmeanfield/interface/equilibrium/stellar_discretization.cppm libmeanfield/interface/equilibrium/stellar_discretization.cppm
libmeanfield/interface/operators/stellar_equilibrium_problem.cppm libmeanfield/interface/operators/stellar_equilibrium_problem.cppm
libmeanfield/interface/seed/stellar_equilibrium_projection.cppm libmeanfield/interface/seed/stellar_equilibrium_projection.cppm
@@ -303,10 +311,12 @@ add_executable(tests
tests/operators/prepared_rotation_displacement_force_affine_deformation.cpp tests/operators/prepared_rotation_displacement_force_affine_deformation.cpp
tests/operators/prepared_displacement_operator.cpp tests/operators/prepared_displacement_operator.cpp
tests/operators/root_manifest.cpp tests/operators/root_manifest.cpp
tests/operators/stellar_equilibrium_compiler.cpp
tests/operators/prepared_central_density.cpp tests/operators/prepared_central_density.cpp
tests/operators/prepared_central_density_stellar_equilibrium.cpp tests/operators/prepared_central_density_stellar_equilibrium.cpp
tests/models/model_specifications.cpp tests/models/model_specifications.cpp
tests/models/typed_stellar_model.cpp tests/models/typed_stellar_model.cpp
tests/models/physics_specification_frontend.cpp
tests/models/stellar_model.cpp tests/models/stellar_model.cpp
tests/operators/stellar_equilibrium_system.cpp tests/operators/stellar_equilibrium_system.cpp
tests/deformation/contracts.cpp tests/deformation/contracts.cpp
@@ -315,6 +325,7 @@ add_executable(tests
tests/deformation/radial_extensions.cpp tests/deformation/radial_extensions.cpp
tests/deformation/domain_deformation.cpp tests/deformation/domain_deformation.cpp
tests/operators/prepared_mass_normalization.cpp tests/operators/prepared_mass_normalization.cpp
tests/operators/prepared_angular_momentum.cpp
tests/operators/prepared_stellar_equilibrium.cpp tests/operators/prepared_stellar_equilibrium.cpp
tests/utils/domain.cpp tests/utils/domain.cpp
tests/field/field_base.cpp tests/field/field_base.cpp
@@ -327,8 +338,12 @@ add_executable(tests
tests/preconditioning/material_surface.cpp tests/preconditioning/material_surface.cpp
tests/preconditioning/stellar_structure.cpp tests/preconditioning/stellar_structure.cpp
tests/preconditioning/specification_border.cpp tests/preconditioning/specification_border.cpp
tests/extensions/fixed_magnetic_specific_energy.cpp
tests/preconditioning/equilibrium_coordinates.cpp tests/preconditioning/equilibrium_coordinates.cpp
tests/preconditioning/stellar_equilibrium.cpp tests/preconditioning/stellar_equilibrium.cpp
tests/normalization/plan.cpp
tests/normalization/physical_riesz.cpp
tests/normalization/stellar_equilibrium.cpp
tests/user-api/stellar_equilibrium.cpp tests/user-api/stellar_equilibrium.cpp
tests/solver/preconditioning_diagnostics.cpp tests/solver/preconditioning_diagnostics.cpp
) )
@@ -429,3 +444,8 @@ add_custom_target(
DEPENDS mpi_tests DEPENDS mpi_tests
USES_TERMINAL USES_TERMINAL
) )
# A deliberately separate, physics-developer-facing example. Its targets
# depend on MeanField, but none of its sources are part of the mean_field
# library or the main regression-test executable.
add_subdirectory(extension_example)

View File

@@ -228,8 +228,7 @@ TEST_CASE(
announce(communicator, "P0 extended baseline: preparing the complete equilibrium operator"); announce(communicator, "P0 extended baseline: preparing the complete equilibrium operator");
const Clock::time_point operatorPreparationStart = Clock::now(); const Clock::time_point operatorPreparationStart = Clock::now();
const operators::PreparedCentralDensityStellarEquilibriumReport preparation = const auto preparation = problem.Prepare(projected.values, make_dependencies(), make_zero_rotation());
problem.Prepare(projected.values, make_dependencies(), make_zero_rotation());
REQUIRE(preparation.assembledResidual); REQUIRE(preparation.assembledResidual);
const double operatorPreparationSeconds = maximum_rank_seconds(operatorPreparationStart, communicator); const double operatorPreparationSeconds = maximum_rank_seconds(operatorPreparationStart, communicator);

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>);
}

View File

@@ -0,0 +1,590 @@
module;
#include <algorithm>
#include <array>
#include <cmath>
#include <utility>
#include <mfem.hpp>
module mean_field;
import :operators.prepared_angular_momentum;
namespace {
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
[[nodiscard]] bool is_vacuum_attribute(const int attribute) {
return DomainSchema::template attribute_belongs_to<mean_field::utils::domain::Vacuum>(attribute);
}
void validate_finite_vector(const mfem::Vector &vector, const char *message) {
for (int index = 0; index < vector.Size(); ++index) {
MFEM_VERIFY(std::isfinite(vector(index)), message);
}
}
void true_to_local(
const mfem::ParFiniteElementSpace &finiteElementSpace,
const mfem::Vector &trueVector,
mfem::Vector &localVector
) {
MFEM_VERIFY(trueVector.Size() == finiteElementSpace.GetTrueVSize(), "True vector has the wrong size.");
localVector.SetSize(finiteElementSpace.GetVSize());
const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix();
if (prolongation != nullptr) {
prolongation->Mult(trueVector, localVector);
} else {
localVector = trueVector;
}
}
[[nodiscard]] const mfem::IntegrationRule &get_moment_of_inertia_rule(
const mean_field::fem::FEM &f,
const mfem::FiniteElement &densityElement,
const mfem::ElementTransformation &transformation
) {
using DensityField = mean_field::field::Field<mean_field::field::Density>;
MFEM_VERIFY(
densityElement.GetOrder() == mean_field::field::Density::Scalar::familyOrder,
"The angular-momentum element does not match the registered density field."
);
const mean_field::quadrature::Query query =
DensityField::make_query<mean_field::field::Density::Form::Quadrupole>(
mean_field::quadrature::QuadratureRole::discretization,
transformation.OrderW(),
std::array<int, 1>{2},
mean_field::utils::DOMAINS::STELLAR,
mean_field::quadrature::MappingKind::general
);
const auto resolution = f.quadratureFactory->get(query, transformation.GetGeometryType());
MFEM_VERIFY(
resolution.integration_rule != nullptr,
"The quadrature policy did not return an angular-momentum integration rule."
);
return *resolution.integration_rule;
}
void validate_shared_gravity_revisions(
const mean_field::operators::context::gravity_field::GravityFieldLinearizationContext &gravityContext,
const mean_field::operators::AngularMomentumDependencies &dependencies
) {
MFEM_VERIFY(
gravityContext.IsPrepared(),
"PreparedAngularMomentumOperator requires the shared gravity context to be prepared first."
);
const auto &revisions = gravityContext.GetRevisions();
MFEM_VERIFY(
revisions.discretization.value == dependencies.discretization.revision &&
revisions.density.value == dependencies.density.revision &&
revisions.displacement.value == dependencies.displacement.revision,
"PreparedAngularMomentumOperator received revisions that do not match the shared gravity context."
);
}
void validate_identity_transition(
const mean_field::operators::AngularMomentumDependencyStamp &prepared,
const mean_field::operators::AngularMomentumDependencyStamp &requested,
const char *message
) {
MFEM_VERIFY(prepared.identity == requested.identity || prepared.revision != requested.revision, message);
}
} // namespace
namespace mean_field::operators {
PreparedAngularMomentumOperator::PreparedAngularMomentumOperator(
const fem::FEM &f,
const mapping::DomainMapper &domainMapper,
const context::gravity_field::GravityFieldLinearizationContext &gravityContext,
models::CompiledFixedAngularMomentum constraint
)
: m_fem(f),
m_domainMapper(domainMapper),
m_gravityContext(gravityContext),
m_constraint(std::move(constraint)) {
MFEM_VERIFY(m_fem.mesh != nullptr, "PreparedAngularMomentumOperator requires a mesh.");
MFEM_VERIFY(
m_fem.mesh->Dimension() == 3 && m_domainMapper.GetDimension() == 3,
"PreparedAngularMomentumOperator currently requires a three-dimensional mapped domain."
);
MFEM_VERIFY(
m_fem.densityFes != nullptr && m_fem.displacementFes != nullptr &&
m_fem.compactificationFes != nullptr && m_fem.compactificationCoordinate != nullptr &&
m_fem.quadratureFactory != nullptr,
"PreparedAngularMomentumOperator requires density, displacement, compactification, and quadrature data."
);
MFEM_VERIFY(
m_gravityContext.GetDensityMap().full_size() == m_fem.densityFes->GetTrueVSize() &&
m_gravityContext.GetDisplacementMap().full_size() == m_fem.displacementFes->GetTrueVSize(),
"PreparedAngularMomentumOperator received incompatible shared FieldDof maps."
);
m_densityVariationTrue.SetSize(m_gravityContext.GetDensityMap().full_size());
m_displacementVariationTrue.SetSize(m_gravityContext.GetDisplacementMap().full_size());
}
PreparedAngularMomentumReport PreparedAngularMomentumOperator::Prepare(
const double angularVelocity,
const AngularMomentumDependencies &dependencies
) {
MFEM_VERIFY(
std::isfinite(angularVelocity),
"PreparedAngularMomentumOperator requires a finite angular-velocity coordinate."
);
validate_shared_gravity_revisions(m_gravityContext, dependencies);
if (m_isPrepared) {
validate_identity_transition(
m_preparedDependencies.discretization,
dependencies.discretization,
"A new angular-momentum discretization identity must change its revision."
);
validate_identity_transition(
m_preparedDependencies.density,
dependencies.density,
"A new angular-momentum density identity must change its revision."
);
validate_identity_transition(
m_preparedDependencies.displacement,
dependencies.displacement,
"A new angular-momentum displacement identity must change its revision."
);
validate_identity_transition(
m_preparedDependencies.rotation,
dependencies.rotation,
"A new angular-momentum rotation identity must change its revision."
);
}
const bool rebuildStaticPlan =
!m_isPrepared || dependencies.discretization != m_preparedDependencies.discretization;
const bool refreshGeometry =
rebuildStaticPlan || dependencies.displacement != m_preparedDependencies.displacement;
const bool refreshDensity = rebuildStaticPlan || dependencies.density != m_preparedDependencies.density;
const bool updateAngularVelocity =
!m_isPrepared || dependencies.rotation != m_preparedDependencies.rotation ||
angularVelocity != m_angularVelocity;
m_isPrepared = false;
PreparedAngularMomentumReport report;
if (rebuildStaticPlan) {
BuildStaticPlan();
report.rebuiltStaticPlan = true;
}
if (refreshGeometry) {
RefreshGeometry(m_gravityContext.GetGeometryContext().GetDisplacementTrue());
report.refreshedGeometry = true;
}
if (refreshDensity) {
RefreshDensity(m_gravityContext.GetDensityTrue());
report.refreshedDensity = true;
}
if (updateAngularVelocity) {
m_angularVelocity = angularVelocity;
report.updatedAngularVelocity = true;
}
if (refreshGeometry || refreshDensity || updateAngularVelocity) {
AssembleResidual();
report.assembledResidual = true;
}
m_preparedDependencies = dependencies;
m_isPrepared = true;
return report;
}
void PreparedAngularMomentumOperator::BuildStaticPlan() {
m_elements.clear();
m_elements.reserve(m_fem.mesh->GetNE());
int localStellarElementCount = 0;
for (int elementId = 0; elementId < m_fem.mesh->GetNE(); ++elementId) {
mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(elementId);
MFEM_VERIFY(transformation != nullptr, "Angular-momentum preparation received a null transformation.");
if (is_vacuum_attribute(transformation->Attribute)) {
continue;
}
++localStellarElementCount;
m_elements.emplace_back();
ElementPAData &data = m_elements.back();
data.elementId = elementId;
data.densityDofTransformation = m_fem.densityFes->GetElementDofs(elementId, data.densityDofs);
data.displacementDofTransformation =
m_fem.displacementFes->GetElementVDofs(elementId, data.displacementDofs);
data.compactificationDofTransformation =
m_fem.compactificationFes->GetElementDofs(elementId, data.compactificationDofs);
const mfem::FiniteElement &densityElement = *m_fem.densityFes->GetFE(elementId);
const mfem::IntegrationRule &integrationRule =
get_moment_of_inertia_rule(m_fem, densityElement, *transformation);
data.quadraturePoints.resize(integrationRule.GetNPoints());
for (int quadraturePoint = 0; quadraturePoint < integrationRule.GetNPoints(); ++quadraturePoint) {
QuadraturePointData &point = data.quadraturePoints[quadraturePoint];
point.integrationPoint = integrationRule.IntPoint(quadraturePoint);
point.densityShape.SetSize(densityElement.GetDof());
densityElement.CalcShape(point.integrationPoint, point.densityShape);
}
}
int globalStellarElementCount = 0;
MPI_Allreduce(
&localStellarElementCount,
&globalStellarElementCount,
1,
MPI_INT,
MPI_SUM,
m_fem.mesh->GetComm()
);
MFEM_VERIFY(globalStellarElementCount > 0, "PreparedAngularMomentumOperator found no stellar elements.");
}
void PreparedAngularMomentumOperator::RefreshGeometry(const mfem::Vector &displacement) {
MFEM_VERIFY(
displacement.Size() == m_fem.displacementFes->GetTrueVSize(),
"Angular-momentum geometry has the wrong displacement size."
);
validate_finite_vector(displacement, "Angular-momentum geometry contains a non-finite displacement.");
mfem::Vector displacementLocal;
true_to_local(*m_fem.displacementFes, displacement, displacementLocal);
mapping::DomainMapper::Workspace workspace(m_fem.mesh->Dimension());
for (ElementPAData &data : m_elements) {
displacementLocal.GetSubVector(data.displacementDofs, data.baseDisplacement);
m_fem.compactificationCoordinate->GetSubVector(data.compactificationDofs, data.compactification);
if (data.displacementDofTransformation != nullptr) {
data.displacementDofTransformation->InvTransformPrimal(data.baseDisplacement);
}
if (data.compactificationDofTransformation != nullptr) {
data.compactificationDofTransformation->InvTransformPrimal(data.compactification);
}
const mfem::FiniteElement &displacementElement = *m_fem.displacementFes->GetFE(data.elementId);
const mfem::FiniteElement &compactificationElement = *m_fem.compactificationFes->GetFE(data.elementId);
const mapping::ElementDisplacementData displacementData =
mapping::ElementDisplacementDataFromElementVDofs(displacementElement, data.baseDisplacement);
const mapping::ElementCompactificationData compactificationData(
compactificationElement,
data.compactification
);
const mapping::ElementMappingData mappingData{
.displacement = displacementData,
.compactification = compactificationData
};
mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(data.elementId);
for (QuadraturePointData &point : data.quadraturePoints) {
const mapping::MappingStatus status = m_domainMapper.EvaluateVolume(
mappingData,
*transformation,
point.integrationPoint,
workspace,
point.mappingContext
);
MFEM_VERIFY(
status == mapping::MappingStatus::valid && !point.mappingContext.mapping.compactified,
"Mapped angular-momentum geometry is invalid. Element: " << data.elementId
);
point.cylindricalRadiusSquared =
CylindricalRadiusSquared(point.mappingContext.mapping.physical_position);
}
}
}
void PreparedAngularMomentumOperator::RefreshDensity(const mfem::Vector &density) {
MFEM_VERIFY(
density.Size() == m_fem.densityFes->GetTrueVSize(),
"Angular-momentum density has the wrong size."
);
validate_finite_vector(density, "Angular-momentum density contains a non-finite value.");
mfem::Vector densityLocal;
true_to_local(*m_fem.densityFes, density, densityLocal);
mfem::Vector elementDensity;
for (ElementPAData &data : m_elements) {
densityLocal.GetSubVector(data.densityDofs, elementDensity);
if (data.densityDofTransformation != nullptr) {
data.densityDofTransformation->InvTransformPrimal(elementDensity);
}
for (QuadraturePointData &point : data.quadraturePoints) {
point.density = elementDensity * point.densityShape;
MFEM_VERIFY(std::isfinite(point.density), "Angular-momentum quadrature density is non-finite.");
}
}
}
void PreparedAngularMomentumOperator::AssembleResidual() {
double localMomentOfInertia = 0.0;
for (const ElementPAData &data : m_elements) {
for (const QuadraturePointData &point : data.quadraturePoints) {
localMomentOfInertia += point.density * point.cylindricalRadiusSquared *
point.mappingContext.quadrature.weight;
}
}
m_momentOfInertia = GlobalSum(localMomentOfInertia);
MFEM_VERIFY(
std::isfinite(m_momentOfInertia) && m_momentOfInertia >= 0.0,
"PreparedAngularMomentumOperator assembled an invalid moment of inertia."
);
m_currentAngularMomentum = m_angularVelocity * m_momentOfInertia;
m_cachedResidual.SetSize(1);
m_cachedResidual(0) = m_currentAngularMomentum - m_constraint.targetAngularMomentum().value();
++m_preparationCount;
}
void PreparedAngularMomentumOperator::BuildResidual(mfem::Vector &residual) const {
VerifyPrepared();
residual = m_cachedResidual;
++m_residualApplicationCount;
}
double PreparedAngularMomentumOperator::EvaluateDensityMomentActionLocal(
const mfem::Vector &densityVariation
) const {
MFEM_VERIFY(
densityVariation.Size() == m_fem.densityFes->GetTrueVSize(),
"Angular-momentum density action has the wrong true-vector size."
);
true_to_local(*m_fem.densityFes, densityVariation, m_densityVariationLocal);
double localAction = 0.0;
for (const ElementPAData &data : m_elements) {
m_densityVariationLocal.GetSubVector(data.densityDofs, m_elementDensityVariation);
if (data.densityDofTransformation != nullptr) {
data.densityDofTransformation->InvTransformPrimal(m_elementDensityVariation);
}
for (const QuadraturePointData &point : data.quadraturePoints) {
localAction += (m_elementDensityVariation * point.densityShape) *
point.cylindricalRadiusSquared * point.mappingContext.quadrature.weight;
}
}
return localAction;
}
double PreparedAngularMomentumOperator::EvaluateDisplacementMomentActionLocal(
const mfem::Vector &displacementVariation
) const {
MFEM_VERIFY(
displacementVariation.Size() == m_fem.displacementFes->GetTrueVSize(),
"Angular-momentum displacement action has the wrong true-vector size."
);
true_to_local(*m_fem.displacementFes, displacementVariation, m_displacementVariationLocal);
mapping::DomainMapper::Workspace workspace(m_fem.mesh->Dimension());
mapping::VolumeMappingVariation variation;
double localAction = 0.0;
for (const ElementPAData &data : m_elements) {
m_displacementVariationLocal.GetSubVector(data.displacementDofs, m_elementDisplacementVariation);
if (data.displacementDofTransformation != nullptr) {
data.displacementDofTransformation->InvTransformPrimal(m_elementDisplacementVariation);
}
const mfem::FiniteElement &displacementElement = *m_fem.displacementFes->GetFE(data.elementId);
const mfem::FiniteElement &compactificationElement = *m_fem.compactificationFes->GetFE(data.elementId);
const mapping::ElementDisplacementData baseDisplacementData =
mapping::ElementDisplacementDataFromElementVDofs(displacementElement, data.baseDisplacement);
const mapping::ElementDisplacementData directionData =
mapping::ElementDisplacementDataFromElementVDofs(displacementElement, m_elementDisplacementVariation);
const mapping::ElementCompactificationData compactificationData(
compactificationElement,
data.compactification
);
const mapping::ElementMappingData mappingData{
.displacement = baseDisplacementData,
.compactification = compactificationData
};
mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(data.elementId);
for (const QuadraturePointData &point : data.quadraturePoints) {
const mapping::MappingStatus status = m_domainMapper.EvaluateVolumeVariation(
mappingData,
directionData,
*transformation,
point.integrationPoint,
point.mappingContext,
workspace,
variation
);
MFEM_VERIFY(
status == mapping::MappingStatus::valid,
"Mapped angular-momentum variation is invalid. Element: " << data.elementId
);
const double radiusSquaredVariation = CylindricalRadiusSquaredVariation(
point.mappingContext.mapping.physical_position,
variation.mapping.physical_position_variation
);
localAction += point.density *
(radiusSquaredVariation * point.mappingContext.quadrature.weight +
point.cylindricalRadiusSquared * variation.weight_variation);
}
}
return localAction;
}
void PreparedAngularMomentumOperator::ApplyDensityJacobianAction(
const mfem::Vector &densityVariation,
mfem::Vector &action
) const {
VerifyPrepared();
MFEM_VERIFY(
densityVariation.Size() == m_gravityContext.GetDensityMap().reduced_size(),
"Angular-momentum density action has the wrong reduced size."
);
validate_finite_vector(densityVariation, "Angular-momentum density direction is non-finite.");
m_gravityContext.GetDensityMap().scatter(densityVariation, m_densityVariationTrue);
action.SetSize(1);
action(0) = m_angularVelocity * GlobalSum(EvaluateDensityMomentActionLocal(m_densityVariationTrue));
++m_actionStatistics.densityApplications;
}
void PreparedAngularMomentumOperator::ApplyDisplacementJacobianAction(
const mfem::Vector &displacementVariation,
mfem::Vector &action
) const {
VerifyPrepared();
MFEM_VERIFY(
displacementVariation.Size() == m_gravityContext.GetDisplacementMap().reduced_size(),
"Angular-momentum displacement action has the wrong reduced size."
);
validate_finite_vector(displacementVariation, "Angular-momentum displacement direction is non-finite.");
m_gravityContext.GetDisplacementMap().scatter(displacementVariation, m_displacementVariationTrue);
action.SetSize(1);
action(0) = m_angularVelocity *
GlobalSum(EvaluateDisplacementMomentActionLocal(m_displacementVariationTrue));
++m_actionStatistics.displacementApplications;
}
void PreparedAngularMomentumOperator::ApplyAngularVelocityJacobianAction(
const double angularVelocityVariation,
mfem::Vector &action
) const {
VerifyPrepared();
MFEM_VERIFY(std::isfinite(angularVelocityVariation), "Angular-velocity direction is non-finite.");
action.SetSize(1);
action(0) = m_momentOfInertia * angularVelocityVariation;
++m_actionStatistics.angularVelocityApplications;
}
void PreparedAngularMomentumOperator::ApplyCompleteJacobianAction(
const mfem::Vector &densityVariation,
const mfem::Vector &displacementVariation,
const double angularVelocityVariation,
mfem::Vector &action
) const {
VerifyPrepared();
MFEM_VERIFY(
densityVariation.Size() == m_gravityContext.GetDensityMap().reduced_size() &&
displacementVariation.Size() == m_gravityContext.GetDisplacementMap().reduced_size(),
"Angular-momentum complete action has incompatible reduced coordinates."
);
validate_finite_vector(densityVariation, "Angular-momentum density direction is non-finite.");
validate_finite_vector(displacementVariation, "Angular-momentum displacement direction is non-finite.");
MFEM_VERIFY(std::isfinite(angularVelocityVariation), "Angular-velocity direction is non-finite.");
m_gravityContext.GetDensityMap().scatter(densityVariation, m_densityVariationTrue);
m_gravityContext.GetDisplacementMap().scatter(displacementVariation, m_displacementVariationTrue);
const double localMomentAction = EvaluateDensityMomentActionLocal(m_densityVariationTrue) +
EvaluateDisplacementMomentActionLocal(m_displacementVariationTrue);
action.SetSize(1);
action(0) = m_angularVelocity * GlobalSum(localMomentAction) +
m_momentOfInertia * angularVelocityVariation;
++m_actionStatistics.completeApplications;
}
double PreparedAngularMomentumOperator::CylindricalRadiusSquared(
const mfem::Vector &physicalPosition
) const noexcept {
const auto &axis = m_constraint.specification().axis();
const auto &center = m_constraint.specification().center();
double radiusSquared = 0.0;
double axialPosition = 0.0;
for (int component = 0; component < 3; ++component) {
const double relative = physicalPosition(component) - center[static_cast<std::size_t>(component)];
radiusSquared += relative * relative;
axialPosition += axis[static_cast<std::size_t>(component)] * relative;
}
return std::max(0.0, radiusSquared - axialPosition * axialPosition);
}
double PreparedAngularMomentumOperator::CylindricalRadiusSquaredVariation(
const mfem::Vector &physicalPosition,
const mfem::Vector &physicalPositionVariation
) const noexcept {
const auto &axis = m_constraint.specification().axis();
const auto &center = m_constraint.specification().center();
double relativeDotVariation = 0.0;
double axialPosition = 0.0;
double axialVariation = 0.0;
for (int component = 0; component < 3; ++component) {
const double relative = physicalPosition(component) - center[static_cast<std::size_t>(component)];
relativeDotVariation += relative * physicalPositionVariation(component);
axialPosition += axis[static_cast<std::size_t>(component)] * relative;
axialVariation += axis[static_cast<std::size_t>(component)] * physicalPositionVariation(component);
}
return 2.0 * (relativeDotVariation - axialPosition * axialVariation);
}
double PreparedAngularMomentumOperator::GlobalSum(const double localValue) const {
double globalValue = 0.0;
MPI_Allreduce(&localValue, &globalValue, 1, MPI_DOUBLE, MPI_SUM, m_fem.mesh->GetComm());
return globalValue;
}
bool PreparedAngularMomentumOperator::IsPrepared() const noexcept {
if (!m_isPrepared || !m_gravityContext.IsPrepared()) {
return false;
}
const auto &revisions = m_gravityContext.GetRevisions();
return revisions.discretization.value == m_preparedDependencies.discretization.revision &&
revisions.density.value == m_preparedDependencies.density.revision &&
revisions.displacement.value == m_preparedDependencies.displacement.revision;
}
double PreparedAngularMomentumOperator::GetMomentOfInertia() const {
VerifyPrepared();
return m_momentOfInertia;
}
double PreparedAngularMomentumOperator::GetAngularVelocity() const {
VerifyPrepared();
return m_angularVelocity;
}
double PreparedAngularMomentumOperator::GetCurrentAngularMomentum() const {
VerifyPrepared();
return m_currentAngularMomentum;
}
double PreparedAngularMomentumOperator::GetTargetAngularMomentum() const noexcept {
return m_constraint.targetAngularMomentum().value();
}
physics::RigidRotation PreparedAngularMomentumOperator::GetRotation() const {
VerifyPrepared();
return m_constraint.makeRotation(m_angularVelocity);
}
AngularMomentumConstraintReport PreparedAngularMomentumOperator::GetConstraintReport() const {
VerifyPrepared();
const double target = GetTargetAngularMomentum();
const double residual = m_currentAngularMomentum - target;
return {
.targetAngularMomentum = target,
.achievedAngularMomentum = m_currentAngularMomentum,
.momentOfInertia = m_momentOfInertia,
.angularVelocity = m_angularVelocity,
.dimensionalResidual = residual,
.scaledResidual = residual / std::max(std::abs(target), 1.0e-300)
};
}
std::uint64_t PreparedAngularMomentumOperator::GetPreparationCount() const noexcept {
return m_preparationCount;
}
std::uint64_t PreparedAngularMomentumOperator::GetResidualApplicationCount() const noexcept {
return m_residualApplicationCount;
}
const PreparedAngularMomentumActionStatistics &
PreparedAngularMomentumOperator::GetActionStatistics() const noexcept {
return m_actionStatistics;
}
const models::CompiledFixedAngularMomentum &
PreparedAngularMomentumOperator::GetCompiledConstraint() const noexcept {
return m_constraint;
}
void PreparedAngularMomentumOperator::VerifyPrepared() const {
MFEM_VERIFY(IsPrepared(), "The angular-momentum invariant must be prepared before application.");
}
} // namespace mean_field::operators

View File

@@ -1,223 +0,0 @@
module;
#include <array>
#include <cmath>
#include <cstdint>
#include <memory>
#include <utility>
#include <mfem.hpp>
module mean_field;
import :operators.prepared_central_density_stellar_equilibrium;
namespace {
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
using PhysicalForm = mean_field::utils::blocks::surface_deformed_stellar_equilibrium_form;
using BorderedForm = mean_field::operators::CentralDensityStellarEquilibriumForm;
[[nodiscard]] std::array<
int,
BorderedForm::value_block_count>
make_value_sizes(const mean_field::operators::StellarEquilibriumLayout &physicalLayout) {
std::array<int, BorderedForm::value_block_count> sizes{};
for (int block = 0; block < PhysicalForm::value_block_count; ++block) {
sizes[block] = physicalLayout.value_offsets()[block + 1] - physicalLayout.value_offsets()[block];
}
sizes[PhysicalForm::value_block_count] = 1;
return sizes;
}
[[nodiscard]] std::array<
int,
BorderedForm::residual_block_count>
make_residual_sizes(const mean_field::operators::StellarEquilibriumLayout &physicalLayout) {
std::array<int, BorderedForm::residual_block_count> sizes{};
for (int block = 0; block < PhysicalForm::residual_block_count; ++block) {
sizes[block] = physicalLayout.residual_offsets()[block + 1] - physicalLayout.residual_offsets()[block];
}
sizes[PhysicalForm::residual_block_count] = 1;
return sizes;
}
[[nodiscard]] mean_field::operators::CentralDensityDependencies
make_phase_dependencies(const mean_field::operators::StellarEquilibriumDependencies &dependencies) {
return {.enthalpy = {.identity = dependencies.enthalpy.identity, .revision = dependencies.enthalpy.revision}};
}
void validate_finite_scalar(
const double value,
const char *message
) {
MFEM_VERIFY(std::isfinite(value), message);
}
} // namespace
namespace mean_field::operators {
field::FieldPointDofMap PreparedCentralDensityStellarEquilibriumOperator::MakeCenterDofMap(const fem::FEM &f) {
MFEM_VERIFY(
f.mesh != nullptr && f.enthalpyFes != nullptr,
"The central-density phase requires the mesh and enthalpy finite-element space."
);
const field::FieldDofMap enthalpyMap = field::make_field_dof_map<field::Enthalpy, DomainSchema>(*f.enthalpyFes);
mfem::Vector origin(f.mesh->SpaceDimension());
origin = 0.0;
return field::make_field_point_dof_map<field::Enthalpy>(*f.enthalpyFes, enthalpyMap, origin, 1.0e-12);
}
PreparedCentralDensityStellarEquilibriumOperator::PreparedCentralDensityStellarEquilibriumOperator(
fem::FEM &f,
std::unique_ptr<PreparedStellarEquilibriumOperator> physicalOperator,
models::CompiledFixedCentralDensity centralDensity,
field::FieldPointDofMap centerDof
)
: mfem::Operator(
physicalOperator->Height() + 1,
physicalOperator->Width() + 1
),
m_physicalOperator(std::move(physicalOperator)),
m_centralDensity(std::move(centralDensity)),
m_phaseConstraint(
std::move(centerDof),
f.mesh->GetComm()
),
m_rootManifest(
make_value_sizes(m_physicalOperator->GetLayout()),
make_residual_sizes(m_physicalOperator->GetLayout()),
m_physicalOperator->GetTargetMass(),
m_physicalOperator->GetSurfaceConstraintOperator().GetPhysicalCondition().targetPressure,
m_physicalOperator->GetSurfaceConstraintOperator().GetSurfaceRows().size(),
CentralDensityManifestInput{
.targetDensity = m_centralDensity.targetDensity().value(),
.targetEnthalpy = m_centralDensity.targetEnthalpy().value(),
.centerDofCount = 1
}
) {
MFEM_VERIFY(
Width() == m_rootManifest.layout().value_offsets().Last() &&
Height() == m_rootManifest.layout().residual_offsets().Last(),
"The central-density bordered root has inconsistent dimensions."
);
}
PreparedCentralDensityStellarEquilibriumReport PreparedCentralDensityStellarEquilibriumOperator::Prepare(
const mfem::Vector &state,
const StellarEquilibriumDependencies &dependencies,
const physics::RigidRotation &rotation
) {
MFEM_VERIFY(state.Size() == Width(), "The central-density bordered root received a state with the wrong size.");
const auto stateView = m_rootManifest.stateView(state);
const mfem::Vector enthalpy = stateView.block(utils::blocks::enthalpy_field.specific_term);
const mfem::Vector border = stateView.block(utils::blocks::fixed_central_density_phase.central_value_term);
validate_finite_scalar(border(0), "The central-density bordered root received a non-finite border value.");
mfem::Vector physicalState(const_cast<mfem::real_t *>(state.GetData()), m_physicalOperator->Width());
m_isPrepared = false;
PreparedCentralDensityStellarEquilibriumReport report;
report.physical = m_physicalOperator->Prepare(physicalState, dependencies, rotation);
report.phase =
m_phaseConstraint.Prepare(m_centralDensity, enthalpy, border(0), make_phase_dependencies(dependencies));
if (report.physical.assembledResidual || report.phase.DidAnyWork() || m_cachedResidual.Size() != Height()) {
AssembleResidual();
report.assembledResidual = true;
}
m_isPrepared = true;
return report;
}
void PreparedCentralDensityStellarEquilibriumOperator::AssembleResidual() {
mfem::Vector physicalResidual;
m_physicalOperator->BuildResidual(physicalResidual);
m_cachedResidual.SetSize(Height());
m_cachedResidual = 0.0;
mfem::Vector physicalDestination(m_cachedResidual.GetData(), physicalResidual.Size());
physicalDestination = physicalResidual;
const auto residualView = m_rootManifest.residualView(m_cachedResidual);
mfem::Vector enthalpyResidual = residualView.block(utils::blocks::enthalpy_field.specific_term);
mfem::Vector phaseResidual = residualView.block(utils::blocks::fixed_central_density_phase.central_value_term);
m_phaseConstraint.AddResidual(enthalpyResidual, phaseResidual);
}
void PreparedCentralDensityStellarEquilibriumOperator::BuildResidual(mfem::Vector &residual) const {
VerifyPrepared();
residual = m_cachedResidual;
}
void PreparedCentralDensityStellarEquilibriumOperator::Mult(
const mfem::Vector &direction,
mfem::Vector &action
) const {
VerifyPrepared();
MFEM_VERIFY(
direction.Size() == Width(), "The central-density bordered root received a direction with the wrong size."
);
const auto directionView = m_rootManifest.directionView(direction);
const mfem::Vector enthalpyDirection = directionView.block(utils::blocks::enthalpy_field.specific_term);
const mfem::Vector borderDirection =
directionView.block(utils::blocks::fixed_central_density_phase.central_value_term);
validate_finite_scalar(
borderDirection(0), "The central-density bordered root received a non-finite border direction."
);
mfem::Vector physicalDirection(const_cast<mfem::real_t *>(direction.GetData()), m_physicalOperator->Width());
mfem::Vector physicalAction;
m_physicalOperator->Mult(physicalDirection, physicalAction);
action.SetSize(Height());
action = 0.0;
mfem::Vector physicalDestination(action.GetData(), physicalAction.Size());
physicalDestination = physicalAction;
const auto actionView = m_rootManifest.residualView(action);
mfem::Vector enthalpyAction = actionView.block(utils::blocks::enthalpy_field.specific_term);
mfem::Vector phaseAction = actionView.block(utils::blocks::fixed_central_density_phase.central_value_term);
m_phaseConstraint.ApplyJacobian(
{.enthalpyVariation = enthalpyDirection, .borderVariation = borderDirection(0)},
{.enthalpyAction = enthalpyAction, .phaseAction = phaseAction}
);
}
bool PreparedCentralDensityStellarEquilibriumOperator::IsPrepared() const noexcept {
return m_isPrepared && m_physicalOperator->IsPrepared() && m_phaseConstraint.IsPrepared();
}
const CentralDensityStellarEquilibriumLayout &
PreparedCentralDensityStellarEquilibriumOperator::GetLayout() const noexcept {
return m_rootManifest.layout();
}
const CentralDensityStellarEquilibriumRootManifest &
PreparedCentralDensityStellarEquilibriumOperator::GetRootManifest() const noexcept {
return m_rootManifest;
}
const PreparedStellarEquilibriumOperator &
PreparedCentralDensityStellarEquilibriumOperator::GetPhysicalOperator() const noexcept {
return *m_physicalOperator;
}
const PreparedCentralDensityConstraint &
PreparedCentralDensityStellarEquilibriumOperator::GetCentralDensityConstraint() const noexcept {
return m_phaseConstraint;
}
RootConstraintReport PreparedCentralDensityStellarEquilibriumOperator::GetFixedMassReport() const {
VerifyPrepared();
return m_physicalOperator->GetFixedMassReport();
}
CentralDensityConstraintReport PreparedCentralDensityStellarEquilibriumOperator::GetCentralDensityReport() const {
VerifyPrepared();
return m_phaseConstraint.GetConstraintReport();
}
void PreparedCentralDensityStellarEquilibriumOperator::VerifyPrepared() const {
MFEM_VERIFY(IsPrepared(), "The central-density bordered root must be prepared before application.");
}
} // namespace mean_field::operators

View File

@@ -903,6 +903,47 @@ namespace mean_field::operators {
++m_algebraicJacobianStatistics.bernoulliConstantApplications; ++m_algebraicJacobianStatistics.bernoulliConstantApplications;
} }
void PreparedHydrostaticEquilibriumOperator::ApplyRotationAmplitudeJacobianAction(
const double fractionalAngularVelocityVariation,
mfem::Vector &action
) const {
VerifyPrepared();
MFEM_VERIFY(
std::isfinite(fractionalAngularVelocityVariation),
"Prepared hydrostatic rotation-amplitude Jacobian received a non-finite variation."
);
mfem::Vector localAction(m_fem.enthalpyFes->GetVSize());
localAction = 0.0;
mfem::Vector weightedVariation;
mfem::Vector elementAction;
for (const ElementPAData &data : m_elements) {
const int quadraturePointCount = data.quadratureWeights.Size();
MFEM_VERIFY(
data.rotationPotential.Size() == quadraturePointCount,
"Prepared hydrostatic rotation-amplitude Jacobian has stale rotation data."
);
weightedVariation.SetSize(quadraturePointCount);
for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) {
weightedVariation(quadraturePoint) =
-2.0 * fractionalAngularVelocityVariation * data.quadratureWeights(quadraturePoint) *
data.rotationPotential(quadraturePoint);
}
elementAction.SetSize(data.enthalpyDofs.Size());
data.enthalpyBasis.MultTranspose(weightedVariation, elementAction);
if (data.enthalpyDofTransformation != nullptr) {
data.enthalpyDofTransformation->TransformDual(elementAction);
}
localAction.AddElementVector(data.enthalpyDofs, elementAction);
}
local_to_true(*m_fem.enthalpyFes, localAction, m_fullEnthalpyAction);
action.SetSize(m_context.GetEnthalpyMap().reduced_size());
m_context.GetEnthalpyMap().gather(m_fullEnthalpyAction, action);
++m_algebraicJacobianStatistics.rotationAmplitudeApplications;
}
void PreparedHydrostaticEquilibriumOperator::ApplyAlgebraicJacobianAction( void PreparedHydrostaticEquilibriumOperator::ApplyAlgebraicJacobianAction(
const mfem::Vector &enthalpyVariation, const mfem::Vector &enthalpyVariation,
const mfem::Vector &gravityPotentialVariation, const mfem::Vector &gravityPotentialVariation,

View File

@@ -356,8 +356,11 @@ namespace mean_field::operators {
m_rootManifest( m_rootManifest(
constructionData.valueSizes, constructionData.valueSizes,
constructionData.residualSizes, constructionData.residualSizes,
fixedMassConstraint.targetMass().value(), StellarEquilibriumSpecificationModel{
surfaceConstraint.descriptor().targetPressure, equationOfState,
surface::Isobaric{
dimensions::PressureValue{surfaceConstraint.descriptor().targetPressure}},
fixedMassConstraint.specification()},
constructionData.pressureSurfaceRows.size() constructionData.pressureSurfaceRows.size()
), ),
m_gravityStateOffsets(constructionData.gravityStateOffsets), m_gravityStateOffsets(constructionData.gravityStateOffsets),
@@ -431,6 +434,7 @@ namespace mean_field::operators {
m_fullMechanicalAction.SetSize(m_domainDeformation.volumeDisplacementSize()); m_fullMechanicalAction.SetSize(m_domainDeformation.volumeDisplacementSize());
m_surfaceShapeAction.SetSize(m_domainDeformation.parameterCount()); m_surfaceShapeAction.SetSize(m_domainDeformation.parameterCount());
m_pullbackDerivativeAction.SetSize(m_domainDeformation.parameterCount()); m_pullbackDerivativeAction.SetSize(m_domainDeformation.parameterCount());
m_densityVolumeIntegralAction.SetSize(1);
m_gravityState = 0.0; m_gravityState = 0.0;
m_gravityDirection = 0.0; m_gravityDirection = 0.0;
@@ -441,6 +445,7 @@ namespace mean_field::operators {
m_fullMechanicalAction = 0.0; m_fullMechanicalAction = 0.0;
m_surfaceShapeAction = 0.0; m_surfaceShapeAction = 0.0;
m_pullbackDerivativeAction = 0.0; m_pullbackDerivativeAction = 0.0;
m_densityVolumeIntegralAction = 0.0;
} }
PreparedStellarEquilibriumReport PreparedStellarEquilibriumOperator::Prepare( PreparedStellarEquilibriumReport PreparedStellarEquilibriumOperator::Prepare(
@@ -494,13 +499,13 @@ namespace mean_field::operators {
const auto rootState = m_rootManifest.stateView(state); const auto rootState = m_rootManifest.stateView(state);
const mfem::Vector reducedDensity = rootState.block(utils::blocks::density_field.mass_term); const auto reducedDensity = rootState.block(utils::blocks::density_field.mass_term);
const mfem::Vector surfaceDeformationParameters = const auto surfaceDeformationParameters =
rootState.block(utils::blocks::surface_deformation_field.parameters_term); rootState.block(utils::blocks::surface_deformation_field.parameters_term);
const mfem::Vector gravityGradient = rootState.block(utils::blocks::gravity_field.gradient_term); const auto gravityGradient = rootState.block(utils::blocks::gravity_field.gradient_term);
const mfem::Vector gravityPotential = rootState.block(utils::blocks::gravity_field.poisson_term); const auto gravityPotential = rootState.block(utils::blocks::gravity_field.poisson_term);
const mfem::Vector reducedEnthalpy = rootState.block(utils::blocks::enthalpy_field.specific_term); const auto reducedEnthalpy = rootState.block(utils::blocks::enthalpy_field.specific_term);
const mfem::Vector bernoulli = const auto bernoulli =
rootState.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term); rootState.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term);
const bool generatedGeometryChanged = const bool generatedGeometryChanged =
@@ -633,13 +638,13 @@ namespace mean_field::operators {
const auto rootDirection = m_rootManifest.directionView(direction); const auto rootDirection = m_rootManifest.directionView(direction);
const mfem::Vector reducedDensityDirection = rootDirection.block(utils::blocks::density_field.mass_term); const auto reducedDensityDirection = rootDirection.block(utils::blocks::density_field.mass_term);
const mfem::Vector surfaceDeformationDirection = const auto surfaceDeformationDirection =
rootDirection.block(utils::blocks::surface_deformation_field.parameters_term); rootDirection.block(utils::blocks::surface_deformation_field.parameters_term);
const mfem::Vector gravityGradientDirection = rootDirection.block(utils::blocks::gravity_field.gradient_term); const auto gravityGradientDirection = rootDirection.block(utils::blocks::gravity_field.gradient_term);
const mfem::Vector gravityPotentialDirection = rootDirection.block(utils::blocks::gravity_field.poisson_term); const auto gravityPotentialDirection = rootDirection.block(utils::blocks::gravity_field.poisson_term);
const mfem::Vector reducedEnthalpyDirection = rootDirection.block(utils::blocks::enthalpy_field.specific_term); const auto reducedEnthalpyDirection = rootDirection.block(utils::blocks::enthalpy_field.specific_term);
const mfem::Vector bernoulliDirection = const auto bernoulliDirection =
rootDirection.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term); rootDirection.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term);
m_domainDeformation.applyJacobian( m_domainDeformation.applyJacobian(
@@ -794,6 +799,41 @@ namespace mean_field::operators {
return m_massNormalizationOperator; return m_massNormalizationOperator;
} }
double PreparedStellarEquilibriumOperator::ApplyDensityVolumeIntegralDensityAction(
const mfem::Vector &densityDirection
) const {
VerifyPrepared();
m_massNormalizationOperator.ApplyDensityJacobianAction(
densityDirection,
m_densityVolumeIntegralAction
);
MFEM_VERIFY(
m_densityVolumeIntegralAction.Size() == 1,
"The density-volume integral must produce one global scalar."
);
return m_densityVolumeIntegralAction(0);
}
double PreparedStellarEquilibriumOperator::ApplyDensityVolumeIntegralSurfaceShapeAction(
const mfem::Vector &surfaceShapeDirection
) const {
VerifyPrepared();
m_domainDeformation.applyJacobian(
m_surfaceDeformationParameters,
surfaceShapeDirection,
m_volumeDisplacementDirection
);
m_massNormalizationOperator.ApplyDisplacementJacobianAction(
m_volumeDisplacementDirection,
m_densityVolumeIntegralAction
);
MFEM_VERIFY(
m_densityVolumeIntegralAction.Size() == 1,
"The density-volume shape derivative must produce one global scalar."
);
return m_densityVolumeIntegralAction(0);
}
const PreparedPressureSurfaceConstraint & const PreparedPressureSurfaceConstraint &
PreparedStellarEquilibriumOperator::GetSurfaceConstraintOperator() const noexcept { PreparedStellarEquilibriumOperator::GetSurfaceConstraintOperator() const noexcept {
return m_surfaceConstraintOperator; return m_surfaceConstraintOperator;

View File

@@ -3,6 +3,7 @@ module;
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <limits> #include <limits>
#include <numbers>
#include <stdexcept> #include <stdexcept>
#include <mfem.hpp> #include <mfem.hpp>
@@ -133,21 +134,15 @@ namespace {
namespace mean_field::seed::detail { namespace mean_field::seed::detail {
ProjectedRadialFields projectRadialFields( ProjectedRadialFields projectRadialFields(
const equilibrium::StellarDiscretization &discretization, fem::FEM &finiteElementModel,
const RadialProfile &profile, const RadialProfile &profile,
const dimensions::MassValue targetMass, const dimensions::MassValue targetMass,
const dimensions::PressureValue targetSurfacePressure,
const StellarEquilibriumProjectionOptions &options const StellarEquilibriumProjectionOptions &options
) { ) {
validate_profile(profile); validate_profile(profile);
if (!std::isfinite(options.surfaceRadiusRelativeTolerance) || options.surfaceRadiusRelativeTolerance < 0.0) { if (!std::isfinite(options.surfaceRadiusRelativeTolerance) || options.surfaceRadiusRelativeTolerance < 0.0) {
throw std::invalid_argument("The surface-radius projection tolerance must be finite and nonnegative."); throw std::invalid_argument("The surface-radius projection tolerance must be finite and nonnegative.");
} }
if (targetSurfacePressure.value() != 0.0) {
throw std::invalid_argument("A Lane-Emden radial seed requires a zero-pressure isobaric surface.");
}
fem::FEM &finiteElementModel = discretization.finiteElementModel();
const SurfaceRadiusRange surfaceRadius = measure_surface_radius(finiteElementModel); const SurfaceRadiusRange surfaceRadius = measure_surface_radius(finiteElementModel);
const double targetRadius = profile.stellarRadius.value(); const double targetRadius = profile.stellarRadius.value();
const double comparisonScale = std::max({targetRadius, surfaceRadius.maximum, 1.0e-300}); const double comparisonScale = std::max({targetRadius, surfaceRadius.maximum, 1.0e-300});
@@ -185,6 +180,20 @@ namespace mean_field::seed::detail {
const physics::GravitySolution gravitySolution = const physics::GravitySolution gravitySolution =
physics::solve_gravity_field(finiteElementModel, options.gravity, densityField, displacementField); physics::solve_gravity_field(finiteElementModel, options.gravity, densityField, displacementField);
double radialMomentIntegral = 0.0;
for (int index = 0; index + 1 < profile.radius.Size(); ++index) {
const double leftRadius = profile.radius(index);
const double rightRadius = profile.radius(index + 1);
const double leftIntegrand = profile.density(index) * std::pow(leftRadius, 4);
const double rightIntegrand = profile.density(index + 1) * std::pow(rightRadius, 4);
radialMomentIntegral +=
0.5 * (rightRadius - leftRadius) * (leftIntegrand + rightIntegrand);
}
const double sphericalMomentOfInertia = (8.0 * std::numbers::pi / 3.0) * radialMomentIntegral;
if (!std::isfinite(sphericalMomentOfInertia) || sphericalMomentOfInertia <= 0.0) {
throw std::runtime_error("The radial profile has no finite, positive moment of inertia.");
}
const field::FieldDofGridFunctionAdapter densityAdapter = const field::FieldDofGridFunctionAdapter densityAdapter =
field::make_field_dof_grid_function_adapter<field::Density, DomainSchema>(*finiteElementModel.densityFes); field::make_field_dof_grid_function_adapter<field::Density, DomainSchema>(*finiteElementModel.densityFes);
const field::FieldDofGridFunctionAdapter enthalpyAdapter = const field::FieldDofGridFunctionAdapter enthalpyAdapter =
@@ -203,7 +212,8 @@ namespace mean_field::seed::detail {
.gravityGradient = gravityFluxAdapter.gather(gravitySolution.gradPhi), .gravityGradient = gravityFluxAdapter.gather(gravitySolution.gradPhi),
.gravityPotential = gravityPotentialAdapter.gather(gravitySolution.phi), .gravityPotential = gravityPotentialAdapter.gather(gravitySolution.phi),
.specificEnthalpy = enthalpyAdapter.gather(enthalpyField), .specificEnthalpy = enthalpyAdapter.gather(enthalpyField),
.bernoulliConstant = -utils::G * targetMass.value() / targetRadius .bernoulliConstant = -utils::G * targetMass.value() / targetRadius,
.sphericalMomentOfInertia = sphericalMomentOfInertia
}; };
} }
} // namespace mean_field::seed::detail } // namespace mean_field::seed::detail

View File

@@ -1,12 +1,16 @@
module; module;
#include <concepts>
#include <memory> #include <memory>
#include <stdexcept> #include <stdexcept>
#include <type_traits>
#include <utility>
export module mean_field:equilibrium.stellar_discretization; export module mean_field:equilibrium.stellar_discretization;
export import :fem; export import :fem;
export import :mapping.domain_mapper; export import :mapping.domain_mapper;
export import :normalization.physical_riesz;
export namespace mean_field::equilibrium { export namespace mean_field::equilibrium {
/* /*
@@ -18,26 +22,78 @@ export namespace mean_field::equilibrium {
* mutable field workspaces. Separating those workspaces is a prerequisite * mutable field workspaces. Separating those workspaces is a prerequisite
* for shared discretization ownership by solved Structure objects. * for shared discretization ownership by solved Structure objects.
*/ */
class StellarDiscretization final { template <normalization::NormalizationPrescription Normalization>
class StellarDiscretizationFor final {
public: public:
explicit StellarDiscretization(fem::FEM &finiteElementModel) using NormalizationPrescriptionType = std::remove_cvref_t<Normalization>;
: StellarDiscretization(
explicit StellarDiscretizationFor(fem::FEM &finiteElementModel)
requires std::same_as<NormalizationPrescriptionType, normalization::Unnormalized>
: StellarDiscretizationFor(
finiteElementModel, finiteElementModel,
RequireDomainMapper(finiteElementModel) RequireDomainMapper(finiteElementModel),
normalization::Unnormalized{}
) { ) {
} }
StellarDiscretization( StellarDiscretizationFor(
fem::FEM &finiteElementModel, fem::FEM &finiteElementModel,
const mapping::DomainMapper &domainMapper const mapping::DomainMapper &domainMapper
)
requires std::same_as<NormalizationPrescriptionType, normalization::Unnormalized>
: StellarDiscretizationFor(
finiteElementModel,
domainMapper,
normalization::Unnormalized{}
) {
}
StellarDiscretizationFor(
fem::FEM &,
mapping::DomainMapper &&
) requires std::same_as<NormalizationPrescriptionType, normalization::Unnormalized> = delete;
StellarDiscretizationFor(
fem::FEM &,
const mapping::DomainMapper &&
) requires std::same_as<NormalizationPrescriptionType, normalization::Unnormalized> = delete;
StellarDiscretizationFor(
fem::FEM &finiteElementModel,
NormalizationPrescriptionType normalizationPrescription
)
: StellarDiscretizationFor(
finiteElementModel,
RequireDomainMapper(finiteElementModel),
std::move(normalizationPrescription)
) {
}
StellarDiscretizationFor(
fem::FEM &finiteElementModel,
const mapping::DomainMapper &domainMapper,
NormalizationPrescriptionType normalizationPrescription
) )
: m_finiteElementModel(std::addressof(finiteElementModel)), : m_finiteElementModel(std::addressof(finiteElementModel)),
m_domainMapper(std::addressof(domainMapper)) { m_domainMapper(std::addressof(domainMapper)),
m_normalizationPrescription(std::move(normalizationPrescription)) {
if (!finiteElementModel.okay()) { if (!finiteElementModel.okay()) {
throw std::invalid_argument("A stellar discretization requires a complete finite-element model."); throw std::invalid_argument("A stellar discretization requires a complete finite-element model.");
} }
} }
StellarDiscretizationFor(
fem::FEM &,
mapping::DomainMapper &&,
NormalizationPrescriptionType
) = delete;
StellarDiscretizationFor(
fem::FEM &,
const mapping::DomainMapper &&,
NormalizationPrescriptionType
) = delete;
[[nodiscard]] fem::FEM &finiteElementModel() const noexcept { [[nodiscard]] fem::FEM &finiteElementModel() const noexcept {
return *m_finiteElementModel; return *m_finiteElementModel;
} }
@@ -46,6 +102,10 @@ export namespace mean_field::equilibrium {
return *m_domainMapper; return *m_domainMapper;
} }
[[nodiscard]] const NormalizationPrescriptionType &normalizationPrescription() const noexcept {
return m_normalizationPrescription;
}
[[nodiscard]] bool isCurrent() const noexcept { [[nodiscard]] bool isCurrent() const noexcept {
return m_finiteElementModel != nullptr && m_domainMapper != nullptr && m_finiteElementModel->okay(); return m_finiteElementModel != nullptr && m_domainMapper != nullptr && m_finiteElementModel->okay();
} }
@@ -60,5 +120,64 @@ export namespace mean_field::equilibrium {
fem::FEM *m_finiteElementModel; fem::FEM *m_finiteElementModel;
const mapping::DomainMapper *m_domainMapper; const mapping::DomainMapper *m_domainMapper;
NormalizationPrescriptionType m_normalizationPrescription;
}; };
template <normalization::NormalizationPrescription Normalization>
StellarDiscretizationFor(fem::FEM &, Normalization)
-> StellarDiscretizationFor<std::remove_cvref_t<Normalization>>;
template <normalization::NormalizationPrescription Normalization>
StellarDiscretizationFor(fem::FEM &, const mapping::DomainMapper &, Normalization)
-> StellarDiscretizationFor<std::remove_cvref_t<Normalization>>;
using StellarDiscretization = StellarDiscretizationFor<normalization::Unnormalized>;
template <typename Candidate> struct IsStellarDiscretization : std::false_type { };
template <normalization::NormalizationPrescription Normalization>
struct IsStellarDiscretization<StellarDiscretizationFor<Normalization>> : std::true_type { };
template <typename Candidate>
concept StellarDiscretizationType = IsStellarDiscretization<std::remove_cvref_t<Candidate>>::value;
template <normalization::NormalizationPrescription Normalization>
[[nodiscard]] auto makeStellarDiscretization(
fem::FEM &finiteElementModel,
Normalization normalizationPrescription
) {
return StellarDiscretizationFor<std::remove_cvref_t<Normalization>>{
finiteElementModel,
std::move(normalizationPrescription)
};
}
template <normalization::NormalizationPrescription Normalization>
[[nodiscard]] auto makeStellarDiscretization(
fem::FEM &finiteElementModel,
const mapping::DomainMapper &domainMapper,
Normalization normalizationPrescription
) {
return StellarDiscretizationFor<std::remove_cvref_t<Normalization>>{
finiteElementModel,
domainMapper,
std::move(normalizationPrescription)
};
}
template <normalization::NormalizationPrescription Normalization>
StellarDiscretizationFor<std::remove_cvref_t<Normalization>>
makeStellarDiscretization(
fem::FEM &,
mapping::DomainMapper &&,
Normalization
) = delete;
template <normalization::NormalizationPrescription Normalization>
StellarDiscretizationFor<std::remove_cvref_t<Normalization>>
makeStellarDiscretization(
fem::FEM &,
const mapping::DomainMapper &&,
Normalization
) = delete;
} // namespace mean_field::equilibrium } // namespace mean_field::equilibrium

View File

@@ -231,6 +231,28 @@ export namespace mean_field::field {
static_assert(constraintsAreValid); static_assert(constraintsAreValid);
}; };
// Scalar angular speed generated by FixedAngularMomentum. The axis and
// center belong to the compiled invariant, so the nonlinear coordinate
// contains only the signed speed along that fixed unit axis.
struct AngularVelocity {
static constexpr std::string_view name = "angular_velocity";
using PhysicalQuantity = dimensions::quantity::AngularVelocity;
using Support = NonSpatialSupport;
struct Scalar final : GlobalScalarQ {
static constexpr std::string_view symbol = "Omega";
};
using Quantities = TypeList<Scalar>;
using Constraints = TypeList<>;
using FormList = TypeList<>;
static constexpr bool constraintsAreValid = validate_constraints(Constraints{});
static_assert(constraintsAreValid);
};
// Solver border generated by FixedCentralDensity. This is deliberately a // Solver border generated by FixedCentralDensity. This is deliberately a
// non-spatial numerical coordinate rather than a physical stellar field. // non-spatial numerical coordinate rather than a physical stellar field.
struct CentralDensityBorder { struct CentralDensityBorder {

View File

@@ -27,6 +27,7 @@ export import :quadrature.mfem;
export import :solver.fields; export import :solver.fields;
export import :solver.preconditioning_diagnostics; export import :solver.preconditioning_diagnostics;
export import :preconditioning; export import :preconditioning;
export import :normalization;
export import :utils.blocks; export import :utils.blocks;
export import :operators.gravity_field; export import :operators.gravity_field;
export import :operators.gravity_field_jacobian; export import :operators.gravity_field_jacobian;
@@ -60,6 +61,7 @@ export import :model.structure.polytropic;
export import :model.specifications; export import :model.specifications;
export import :model.typed_stellar; export import :model.typed_stellar;
export import :model.compiled_fixed_mass; export import :model.compiled_fixed_mass;
export import :model.compiled_fixed_angular_momentum;
export import :model.compiled_fixed_central_density; export import :model.compiled_fixed_central_density;
export import :eos.quantities; export import :eos.quantities;
export import :eos.relations; export import :eos.relations;
@@ -85,11 +87,13 @@ export import :model.stellar;
export import :operators.root_manifest; export import :operators.root_manifest;
export import :operators.prepared_constraint; export import :operators.prepared_constraint;
export import :operators.prepared_mass_normalization; export import :operators.prepared_mass_normalization;
export import :operators.prepared_angular_momentum;
export import :operators.prepared_central_density; export import :operators.prepared_central_density;
export import :operators.prepared_centering_constraint; export import :operators.prepared_centering_constraint;
export import :operators.prepared_surface_constraint; export import :operators.prepared_surface_constraint;
export import :operators.prepared_stellar_equilibrium; export import :operators.prepared_stellar_equilibrium;
export import :operators.prepared_central_density_stellar_equilibrium; export import :operators.stellar_equilibrium_compiler;
export import :operators.prepared_variadic_stellar_equilibrium;
export import :equilibrium.stellar_discretization; export import :equilibrium.stellar_discretization;
export import :operators.stellar_equilibrium_problem; export import :operators.stellar_equilibrium_problem;
export import :seed.stellar_equilibrium_projection; export import :seed.stellar_equilibrium_projection;

View File

@@ -0,0 +1,74 @@
module;
#include <concepts>
#include <cstddef>
#include <type_traits>
#include <mfem.hpp>
export module mean_field:model.compiled_fixed_angular_momentum;
export import :field.registry;
export import :model.compiled_fixed_mass;
export import :physics.rigid_rotation;
export import :utils.blocks;
export namespace mean_field::models {
using FixedAngularMomentumLayoutRequest = ConstraintLayoutRequest<
FixedAngularMomentum,
PhysicalCoordinateFor<FixedAngularMomentum>,
ResidualFor<FixedAngularMomentum>,
utils::blocks::fixed_angular_momentum::angular_velocity::value,
utils::blocks::fixed_angular_momentum::angular_velocity::residual,
utils::blocks::fixed_angular_momentum::angular_velocity,
utils::blocks::density::mass::value,
utils::blocks::surface_deformation::parameters::value,
utils::blocks::fixed_angular_momentum::angular_velocity::value>;
class CompiledFixedAngularMomentum final {
public:
using SpecificationType = FixedAngularMomentum;
using LayoutRequest = FixedAngularMomentumLayoutRequest;
using AngularVelocityType = typename LayoutRequest::GeneratedValueType;
using ResidualType = typename LayoutRequest::GeneratedResidualType;
using AngularVelocityField = field::AngularVelocity;
explicit CompiledFixedAngularMomentum(const FixedAngularMomentum specification) noexcept
: m_specification(specification) {
}
[[nodiscard]] const FixedAngularMomentum &specification() const noexcept {
return m_specification;
}
[[nodiscard]] dimensions::AngularMomentumValue targetAngularMomentum() const noexcept {
return m_specification.targetAngularMomentum();
}
[[nodiscard]] physics::RigidRotation makeRotation(const double angularVelocity) const {
mfem::Vector velocity(3);
mfem::Vector center(3);
for (int component = 0; component < 3; ++component) {
velocity(component) = angularVelocity * m_specification.axis()[static_cast<std::size_t>(component)];
center(component) = m_specification.center()[static_cast<std::size_t>(component)];
}
return {velocity, center};
}
[[nodiscard]] static consteval LayoutRequest layoutRequest() noexcept {
return {};
}
private:
FixedAngularMomentum m_specification;
};
[[nodiscard]] inline CompiledFixedAngularMomentum compileConstraint(
const FixedAngularMomentum specification
) noexcept {
return CompiledFixedAngularMomentum{specification};
}
static_assert(ConstraintLayoutRequestType<FixedAngularMomentumLayoutRequest>);
static_assert(CompiledConstraint<CompiledFixedAngularMomentum>);
} // namespace mean_field::models

File diff suppressed because it is too large Load Diff

View File

@@ -14,21 +14,41 @@ export namespace mean_field::model {
template <typename SpecificationSet> class StellarModel; template <typename SpecificationSet> class StellarModel;
template <models::ModelSpecification... CanonicalSpecifications> template <models::ModelSpecification... CanonicalSpecifications>
requires models::ValidModelSpecificationPack<CanonicalSpecifications...> &&
models::SpecificationOperatorSignature<
models::detail::SpecificationSetStorage<CanonicalSpecifications...>>::symbolicallySquare
class StellarModel<models::detail::SpecificationSetStorage<CanonicalSpecifications...>> final { class StellarModel<models::detail::SpecificationSetStorage<CanonicalSpecifications...>> final {
public: public:
using SpecificationTypes = models::detail::SpecificationSetStorage<CanonicalSpecifications...>; using SpecificationTypes = models::detail::SpecificationSetStorage<CanonicalSpecifications...>;
using OperatorSignature = models::SpecificationOperatorSignature<SpecificationTypes>; using OperatorSignature = models::SpecificationOperatorSignature<SpecificationTypes>;
using Storage = models::Model<CanonicalSpecifications...>; using Storage = models::Model<CanonicalSpecifications...>;
using EquationOfStateType =
models::SpecificationForRoleT<models::SpecificationRole::constitutive_law, SpecificationTypes>;
static constexpr std::size_t specificationCount = sizeof...(CanonicalSpecifications); static constexpr std::size_t specificationCount = sizeof...(CanonicalSpecifications);
static constexpr bool symbolicallySquare = Storage::symbolicallySquare; static constexpr bool symbolicallySquare = Storage::symbolicallySquare;
static constexpr bool hasCompleteEquilibriumCompiler = Storage::hasCompleteRootCompiler; static constexpr bool hasCompleteEquilibriumDeclaration =
static constexpr models::EquilibriumSystemCompilation compilationClass = Storage::compilationClass; Storage::hasCompleteEquilibriumDeclaration;
template <models::SpecificationRole Role>
using SpecificationsForRole = models::SpecificationsForRoleT<Role, SpecificationTypes>;
template <models::SpecificationRole Role>
requires models::HasUniqueSpecificationForRole<Role, SpecificationTypes>
using SpecificationForRole = models::SpecificationForRoleT<Role, SpecificationTypes>;
template <models::SpecificationRole Role>
static constexpr std::size_t specificationRoleCount = models::specificationRoleCount<Role, SpecificationTypes>;
template <models::SpecificationRole Role>
static constexpr bool hasSpecificationsForRole = models::HasSpecificationsForRole<Role, SpecificationTypes>;
template <models::SpecificationRole Role>
static constexpr bool hasUniqueSpecificationForRole =
models::HasUniqueSpecificationForRole<Role, SpecificationTypes>;
template <typename... Arguments> template <typename... Arguments>
requires std::constructible_from< requires std::constructible_from<Storage, Arguments...>
Storage,
Arguments...>
explicit StellarModel(Arguments &&...arguments) : m_specifications(std::forward<Arguments>(arguments)...) { explicit StellarModel(Arguments &&...arguments) : m_specifications(std::forward<Arguments>(arguments)...) {
} }
@@ -41,6 +61,25 @@ export namespace mean_field::model {
template <models::ModelSpecification Specification> template <models::ModelSpecification Specification>
static constexpr bool containsSpecification = Storage::template containsSpecification<Specification>; static constexpr bool containsSpecification = Storage::template containsSpecification<Specification>;
template <models::SpecificationRole Role>
requires models::HasUniqueSpecificationForRole<Role, SpecificationTypes>
[[nodiscard]] const models::SpecificationForRoleT<Role, SpecificationTypes> &
specificationForRole() const noexcept {
using Specification = models::SpecificationForRoleT<Role, SpecificationTypes>;
return specification<Specification>();
}
[[nodiscard]] const EquationOfStateType &equationOfState() const noexcept {
return specificationForRole<models::SpecificationRole::constitutive_law>();
}
template <typename = void>
requires models::HasUniqueSpecificationForRole<models::SpecificationRole::boundary_condition,
SpecificationTypes>
[[nodiscard]] const auto &surfaceCondition() const noexcept {
return specificationForRole<models::SpecificationRole::boundary_condition>();
}
[[nodiscard]] static constexpr std::span<const models::RuntimeSpecificationDescriptor> [[nodiscard]] static constexpr std::span<const models::RuntimeSpecificationDescriptor>
runtimeSpecificationDescriptors() noexcept { runtimeSpecificationDescriptors() noexcept {
return Storage::runtimeSpecificationDescriptors(); return Storage::runtimeSpecificationDescriptors();
@@ -56,11 +95,69 @@ export namespace mean_field::model {
-> StellarModel<models::SpecificationSet<std::remove_cvref_t<Specifications>...>>; -> StellarModel<models::SpecificationSet<std::remove_cvref_t<Specifications>...>>;
namespace detail { namespace detail {
template <typename Candidate> struct IsStellarModel : std::false_type { }; template <typename Candidate, typename = void> struct IsStellarModel : std::false_type {};
template <typename SpecificationSet> struct IsStellarModel<StellarModel<SpecificationSet>> : std::true_type { }; template <typename SpecificationSet>
struct IsStellarModel<
StellarModel<SpecificationSet>,
std::void_t<typename StellarModel<SpecificationSet>::SpecificationTypes,
typename StellarModel<SpecificationSet>::OperatorSignature,
decltype(StellarModel<SpecificationSet>::specificationCount),
decltype(StellarModel<SpecificationSet>::hasCompleteEquilibriumDeclaration)>>
: std::true_type {};
template <models::SpecificationRole Role, typename Candidate, bool = IsStellarModel<Candidate>::value>
struct StellarModelRoleSelection {
using Types = models::ModelTypeList<>;
static constexpr std::size_t count = 0;
};
template <models::SpecificationRole Role, typename Candidate>
struct StellarModelRoleSelection<Role, Candidate, true> {
using Types = models::SpecificationsForRoleT<Role, typename Candidate::SpecificationTypes>;
static constexpr std::size_t count =
models::specificationRoleCount<Role, typename Candidate::SpecificationTypes>;
};
} // namespace detail } // namespace detail
template <typename Candidate> template <typename Candidate>
concept StellarModelType = detail::IsStellarModel<std::remove_cvref_t<Candidate>>::value; concept StellarModelType = detail::IsStellarModel<std::remove_cvref_t<Candidate>>::value;
template <models::SpecificationRole Role, typename Candidate>
inline constexpr std::size_t specificationRoleCount =
detail::StellarModelRoleSelection<Role, std::remove_cvref_t<Candidate>>::count;
template <models::SpecificationRole Role, typename Candidate>
concept HasSpecificationsForRole = StellarModelType<Candidate> && specificationRoleCount<Role, Candidate> > 0;
template <models::SpecificationRole Role, typename Candidate>
concept HasUniqueSpecificationForRole = StellarModelType<Candidate> && specificationRoleCount<Role, Candidate> == 1;
template <models::SpecificationRole Role, typename Candidate>
requires StellarModelType<Candidate>
using SpecificationsForRoleT =
typename detail::StellarModelRoleSelection<Role, std::remove_cvref_t<Candidate>>::Types;
template <models::SpecificationRole Role, typename Candidate>
requires HasUniqueSpecificationForRole<Role, Candidate>
using SpecificationForRoleT =
models::SpecificationForRoleT<Role, typename std::remove_cvref_t<Candidate>::SpecificationTypes>;
template <typename Candidate>
concept HasEquationOfState = HasUniqueSpecificationForRole<models::SpecificationRole::constitutive_law, Candidate>;
template <typename Candidate>
concept HasSurfaceCondition = HasSpecificationsForRole<models::SpecificationRole::boundary_condition, Candidate>;
template <typename Candidate>
concept HasUniqueSurfaceCondition =
HasUniqueSpecificationForRole<models::SpecificationRole::boundary_condition, Candidate>;
template <HasEquationOfState Candidate>
using EquationOfStateType = SpecificationForRoleT<models::SpecificationRole::constitutive_law, Candidate>;
template <HasUniqueSurfaceCondition Candidate>
using SurfaceConditionType = SpecificationForRoleT<models::SpecificationRole::boundary_condition, Candidate>;
} // namespace mean_field::model } // namespace mean_field::model

View File

@@ -0,0 +1,6 @@
export module mean_field:normalization;
export import :normalization.plan;
export import :normalization.physical_riesz;
export import :normalization.operators;
export import :normalization.stellar_equilibrium;

View File

@@ -0,0 +1,621 @@
module;
#include <array>
#include <cmath>
#include <concepts>
#include <cstdint>
#include <span>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#include <mfem.hpp>
export module mean_field:normalization.operators;
export import :normalization.physical_riesz;
export namespace mean_field::normalization {
class DiagonalNormalization final {
public:
DiagonalNormalization(
mfem::Vector stateToNormalized,
mfem::Vector residualToNormalized
)
: m_stateToNormalized(std::move(stateToNormalized)),
m_residualToNormalized(std::move(residualToNormalized)) {
ValidateFactors(m_stateToNormalized, "state");
ValidateFactors(m_residualToNormalized, "residual");
}
[[nodiscard]] static DiagonalNormalization Identity(
const int stateSize,
const int residualSize
) {
if (stateSize < 0 || residualSize < 0) {
throw std::invalid_argument("Normalization dimensions cannot be negative.");
}
mfem::Vector state(stateSize);
mfem::Vector residual(residualSize);
state = 1.0;
residual = 1.0;
return {std::move(state), std::move(residual)};
}
[[nodiscard]] int StateSize() const noexcept {
return m_stateToNormalized.Size();
}
[[nodiscard]] int ResidualSize() const noexcept {
return m_residualToNormalized.Size();
}
[[nodiscard]] const mfem::Vector &StateFactors() const noexcept {
return m_stateToNormalized;
}
[[nodiscard]] const mfem::Vector &ResidualFactors() const noexcept {
return m_residualToNormalized;
}
void NormalizeState(
const mfem::Vector &physical,
mfem::Vector &normalized
) const {
Apply(m_stateToNormalized, physical, normalized, false, "state");
}
void DenormalizeState(
const mfem::Vector &normalized,
mfem::Vector &physical
) const {
Apply(m_stateToNormalized, normalized, physical, true, "state");
}
void NormalizeResidual(
const mfem::Vector &physical,
mfem::Vector &normalized
) const {
Apply(m_residualToNormalized, physical, normalized, false, "residual");
}
void DenormalizeResidual(
const mfem::Vector &normalized,
mfem::Vector &physical
) const {
Apply(m_residualToNormalized, normalized, physical, true, "residual");
}
[[nodiscard]] double LocalStateNormSquared(const mfem::Vector &physical) const {
return LocalNormSquared(m_stateToNormalized, physical, "state");
}
[[nodiscard]] double LocalResidualNormSquared(const mfem::Vector &physical) const {
return LocalNormSquared(m_residualToNormalized, physical, "residual");
}
private:
static void ValidateFactors(
const mfem::Vector &factors,
const char *role
) {
for (int index = 0; index < factors.Size(); ++index) {
if (!std::isfinite(factors(index)) || factors(index) <= 0.0) {
throw std::invalid_argument(
std::string("The ") + role + " normalization factors must be finite and positive."
);
}
}
}
static void Apply(
const mfem::Vector &factors,
const mfem::Vector &input,
mfem::Vector &output,
const bool inverse,
const char *role
) {
if (input.Size() != factors.Size()) {
throw std::invalid_argument(std::string("The ") + role + " vector has the wrong size.");
}
const bool exactAlias = input.GetData() == output.GetData() && input.Size() == output.Size();
if (!exactAlias) {
output.SetSize(input.Size());
}
for (int index = 0; index < input.Size(); ++index) {
const double value = input(index);
output(index) = inverse ? value / factors(index) : factors(index) * value;
}
}
[[nodiscard]] static double LocalNormSquared(
const mfem::Vector &factors,
const mfem::Vector &physical,
const char *role
) {
if (physical.Size() != factors.Size()) {
throw std::invalid_argument(std::string("The ") + role + " vector has the wrong size.");
}
double normSquared = 0.0;
for (int index = 0; index < physical.Size(); ++index) {
const double normalized = factors(index) * physical(index);
normSquared += normalized * normalized;
}
return normSquared;
}
mfem::Vector m_stateToNormalized;
mfem::Vector m_residualToNormalized;
};
/* Detection-safe public operation for an ordinary third-party runtime
* policy. The exact policy is recovered from the problem type and must own
* every method in its compiled plan. Its implementation remains beside
* the policy and is found by ADL, so adding a normalization family does
* not edit a library registry or switch. */
template <typename Problem>
concept RuntimePreparedNormalizationOperation =
requires(const std::remove_cvref_t<Problem> &problem) {
typename std::remove_cvref_t<Problem>::NormalizationPrescriptionType;
typename std::remove_cvref_t<Problem>::FormType;
requires RuntimePreparedNormalizationFor<
typename std::remove_cvref_t<Problem>::NormalizationPrescriptionType,
typename std::remove_cvref_t<Problem>::FormType>;
{
problem.GetNormalizationPrescription()
} -> std::same_as<const typename std::remove_cvref_t<Problem>::NormalizationPrescriptionType &>;
{
prepareStellarNormalization(
problem.GetNormalizationPrescription(),
problem)
} -> std::same_as<DiagonalNormalization>;
};
template <typename Form>
requires utils::blocks::block_form_is_valid_v<Form>
class DiagonalNormalizationBuilder final {
public:
explicit DiagonalNormalizationBuilder(const utils::blocks::form_layout<Form> &layout)
: m_layout(&layout),
m_stateFactors(layout.value_offsets().Last()),
m_residualFactors(layout.residual_offsets().Last()) {
}
explicit DiagonalNormalizationBuilder(
utils::blocks::form_layout<Form> &&
) = delete;
explicit DiagonalNormalizationBuilder(
const utils::blocks::form_layout<Form> &&
) = delete;
template <typename Block>
requires utils::blocks::contains_type_v<Block, typename Form::value_blocks>
void SetValueBlock(
const double physicalScale,
const mfem::Vector &primalGramDiagonal
) {
constexpr int block = utils::blocks::type_index_v<Block, typename Form::value_blocks>;
RequireUnassigned(m_valueAssigned[block], "value");
AssignBlock(
m_stateFactors,
m_layout->value_offsets()[block],
m_layout->value_offsets()[block + 1] - m_layout->value_offsets()[block],
physicalScale,
primalGramDiagonal,
false
);
m_valueAssigned[block] = true;
}
template <typename Block>
requires utils::blocks::contains_type_v<Block, typename Form::residual_blocks>
void SetResidualBlock(
const double physicalScale,
const mfem::Vector &primalGramDiagonal
) {
constexpr int block = utils::blocks::type_index_v<Block, typename Form::residual_blocks>;
RequireUnassigned(m_residualAssigned[block], "residual");
AssignBlock(
m_residualFactors,
m_layout->residual_offsets()[block],
m_layout->residual_offsets()[block + 1] - m_layout->residual_offsets()[block],
physicalScale,
primalGramDiagonal,
true
);
m_residualAssigned[block] = true;
}
template <typename Block>
requires utils::blocks::contains_type_v<Block, typename Form::value_blocks>
void SetValueGlobal(const double physicalScale) {
constexpr int block = utils::blocks::type_index_v<Block, typename Form::value_blocks>;
SetConstantMetricValueBlock<Block>(physicalScale, BlockSize(m_layout->value_offsets(), block));
}
template <typename Block>
requires utils::blocks::contains_type_v<Block, typename Form::residual_blocks>
void SetResidualGlobal(const double physicalScale) {
constexpr int block = utils::blocks::type_index_v<Block, typename Form::residual_blocks>;
mfem::Vector metric(BlockSize(m_layout->residual_offsets(), block));
metric = 1.0;
SetResidualBlock<Block>(physicalScale, metric);
}
template <typename Block>
requires utils::blocks::contains_type_v<Block, typename Form::residual_blocks>
void SetHybridResidualBlock(
const double physicalScale,
const mfem::Vector &bulkPrimalGramDiagonal,
const std::span<const int> pointRows,
const double pointMetric = 1.0
) {
constexpr int block = utils::blocks::type_index_v<Block, typename Form::residual_blocks>;
const int size = BlockSize(m_layout->residual_offsets(), block);
if (bulkPrimalGramDiagonal.Size() != size) {
throw std::invalid_argument("The hybrid residual Gram diagonal has the wrong size.");
}
ValidateMetric(pointMetric);
std::vector<bool> isPointRow(static_cast<std::size_t>(size), false);
for (const int row : pointRows) {
if (row < 0 || row >= size) {
throw std::out_of_range("A hybrid point row lies outside its residual block.");
}
if (isPointRow[static_cast<std::size_t>(row)]) {
throw std::invalid_argument("A hybrid point row was supplied more than once.");
}
isPointRow[static_cast<std::size_t>(row)] = true;
}
mfem::Vector metric(size);
for (int row = 0; row < size; ++row) {
metric(row) = isPointRow[static_cast<std::size_t>(row)]
? pointMetric
: bulkPrimalGramDiagonal(row);
}
SetResidualBlock<Block>(physicalScale, metric);
}
[[nodiscard]] DiagonalNormalization Build() && {
for (const bool assigned : m_valueAssigned) {
if (!assigned) {
throw std::logic_error("The normalization is missing a value block.");
}
}
for (const bool assigned : m_residualAssigned) {
if (!assigned) {
throw std::logic_error("The normalization is missing a residual block.");
}
}
return {std::move(m_stateFactors), std::move(m_residualFactors)};
}
private:
template <typename Block>
void SetConstantMetricValueBlock(
const double physicalScale,
const int size
) {
mfem::Vector metric(size);
metric = 1.0;
SetValueBlock<Block>(physicalScale, metric);
}
[[nodiscard]] static int BlockSize(
const mfem::Array<int> &offsets,
const int block
) noexcept {
return offsets[block + 1] - offsets[block];
}
static void RequireUnassigned(
const bool assigned,
const char *role
) {
if (assigned) {
throw std::logic_error(std::string("The ") + role + " block normalization was assigned twice.");
}
}
static void ValidateMetric(const double metric) {
if (!std::isfinite(metric) || metric <= 0.0) {
throw std::invalid_argument("Every Riesz Gram diagonal entry must be finite and positive.");
}
}
static void AssignBlock(
mfem::Vector &factors,
const int offset,
const int size,
const double physicalScale,
const mfem::Vector &primalGramDiagonal,
const bool dual
) {
if (!std::isfinite(physicalScale) || physicalScale <= 0.0) {
throw std::invalid_argument("A physical normalization scale must be finite and positive.");
}
if (primalGramDiagonal.Size() != size) {
throw std::invalid_argument("A Riesz Gram diagonal has the wrong block size.");
}
for (int index = 0; index < size; ++index) {
const double metric = primalGramDiagonal(index);
ValidateMetric(metric);
const double rieszFactor = std::sqrt(metric);
const double factor = dual
? 1.0 / (physicalScale * rieszFactor)
: rieszFactor / physicalScale;
if (!std::isfinite(factor) || factor <= 0.0) {
throw std::overflow_error("A normalization factor is not finite and positive.");
}
factors(offset + index) = factor;
}
}
const utils::blocks::form_layout<Form> *m_layout;
mfem::Vector m_stateFactors;
mfem::Vector m_residualFactors;
std::array<bool, Form::value_block_count> m_valueAssigned{};
std::array<bool, Form::residual_block_count> m_residualAssigned{};
};
class ScaledJacobianOperator final : public mfem::Operator {
public:
ScaledJacobianOperator(
const mfem::Operator &physicalJacobian,
const DiagonalNormalization &normalization
)
: mfem::Operator(normalization.ResidualSize(), normalization.StateSize()),
m_physicalJacobian(&physicalJacobian),
m_normalization(&normalization),
m_physicalDirection(normalization.StateSize()),
m_physicalAction(normalization.ResidualSize()) {
if (physicalJacobian.Width() != normalization.StateSize() ||
physicalJacobian.Height() != normalization.ResidualSize()) {
throw std::invalid_argument("The physical Jacobian and normalization dimensions do not agree.");
}
}
ScaledJacobianOperator(
mfem::Operator &&,
const DiagonalNormalization &
) = delete;
ScaledJacobianOperator(
const mfem::Operator &&,
const DiagonalNormalization &
) = delete;
ScaledJacobianOperator(
const mfem::Operator &,
DiagonalNormalization &&
) = delete;
ScaledJacobianOperator(
const mfem::Operator &,
const DiagonalNormalization &&
) = delete;
void Mult(
const mfem::Vector &normalizedDirection,
mfem::Vector &normalizedAction
) const override {
m_normalization->DenormalizeState(normalizedDirection, m_physicalDirection);
m_physicalJacobian->Mult(m_physicalDirection, m_physicalAction);
m_normalization->NormalizeResidual(m_physicalAction, normalizedAction);
}
private:
const mfem::Operator *m_physicalJacobian;
const DiagonalNormalization *m_normalization;
mutable mfem::Vector m_physicalDirection;
mutable mfem::Vector m_physicalAction;
};
class ScaledInverseOperator final : public mfem::Operator {
public:
ScaledInverseOperator(
const mfem::Operator &physicalInverse,
const DiagonalNormalization &normalization
)
: mfem::Operator(normalization.StateSize(), normalization.ResidualSize()),
m_physicalInverse(&physicalInverse),
m_normalization(&normalization),
m_physicalResidual(normalization.ResidualSize()),
m_physicalCorrection(normalization.StateSize()) {
if (physicalInverse.Width() != normalization.ResidualSize() ||
physicalInverse.Height() != normalization.StateSize()) {
throw std::invalid_argument("The physical inverse and normalization dimensions do not agree.");
}
}
ScaledInverseOperator(
mfem::Operator &&,
const DiagonalNormalization &
) = delete;
ScaledInverseOperator(
const mfem::Operator &&,
const DiagonalNormalization &
) = delete;
ScaledInverseOperator(
const mfem::Operator &,
DiagonalNormalization &&
) = delete;
ScaledInverseOperator(
const mfem::Operator &,
const DiagonalNormalization &&
) = delete;
void Mult(
const mfem::Vector &normalizedResidual,
mfem::Vector &normalizedCorrection
) const override {
m_normalization->DenormalizeResidual(normalizedResidual, m_physicalResidual);
m_physicalInverse->Mult(m_physicalResidual, m_physicalCorrection);
m_normalization->NormalizeState(m_physicalCorrection, normalizedCorrection);
}
private:
const mfem::Operator *m_physicalInverse;
const DiagonalNormalization *m_normalization;
mutable mfem::Vector m_physicalResidual;
mutable mfem::Vector m_physicalCorrection;
};
struct ScaledPreconditionerStatistics final {
std::uint64_t operatorBindings{0};
std::uint64_t applications{0};
};
/*
* Solver-compatible realization of R^{-1} M^{-1} L^{-1}. The wrapped
* inverse always sees the dimensional Jacobian, even when an MFEM Krylov
* solver binds this object to the normalized Jacobian L J R.
*/
class ScaledPreconditioner final : public mfem::Solver {
public:
ScaledPreconditioner(
mfem::Solver &physicalInverse,
const mfem::Operator &physicalJacobian,
const mfem::Operator &normalizedJacobian,
const DiagonalNormalization &normalization
)
: mfem::Solver(
normalization.StateSize(),
normalization.ResidualSize(),
physicalInverse.iterative_mode
),
m_physicalInverse(&physicalInverse),
m_physicalJacobian(&physicalJacobian),
m_expectedNormalizedJacobian(&normalizedJacobian),
m_normalization(&normalization),
m_physicalResidual(normalization.ResidualSize()),
m_physicalCorrection(normalization.StateSize()) {
if (physicalInverse.Width() != normalization.ResidualSize() ||
physicalInverse.Height() != normalization.StateSize() ||
physicalJacobian.Width() != normalization.StateSize() ||
physicalJacobian.Height() != normalization.ResidualSize()) {
throw std::invalid_argument(
"The physical preconditioner, Jacobian, and normalization dimensions do not agree."
);
}
SetOperator(normalizedJacobian);
}
ScaledPreconditioner(
mfem::Solver &,
mfem::Operator &&,
const mfem::Operator &,
const DiagonalNormalization &
) = delete;
ScaledPreconditioner(
mfem::Solver &,
const mfem::Operator &&,
const mfem::Operator &,
const DiagonalNormalization &
) = delete;
ScaledPreconditioner(
mfem::Solver &,
const mfem::Operator &,
mfem::Operator &&,
const DiagonalNormalization &
) = delete;
ScaledPreconditioner(
mfem::Solver &,
const mfem::Operator &,
const mfem::Operator &&,
const DiagonalNormalization &
) = delete;
ScaledPreconditioner(
mfem::Solver &,
const mfem::Operator &,
const mfem::Operator &,
DiagonalNormalization &&
) = delete;
ScaledPreconditioner(
mfem::Solver &,
const mfem::Operator &,
const mfem::Operator &,
const DiagonalNormalization &&
) = delete;
ScaledPreconditioner(const ScaledPreconditioner &) = delete;
ScaledPreconditioner &operator=(const ScaledPreconditioner &) = delete;
ScaledPreconditioner(ScaledPreconditioner &&) = delete;
ScaledPreconditioner &operator=(ScaledPreconditioner &&) = delete;
void SetOperator(const mfem::Operator &normalizedJacobian) override {
if (normalizedJacobian.Width() != Width() || normalizedJacobian.Height() != Height()) {
throw std::invalid_argument(
"The scaled preconditioner received an incompatible normalized Jacobian."
);
}
if (&normalizedJacobian != m_expectedNormalizedJacobian) {
throw std::invalid_argument(
"The scaled preconditioner cannot be rebound to a different normalized Jacobian."
);
}
m_physicalInverse->SetOperator(*m_physicalJacobian);
m_normalizedJacobian = &normalizedJacobian;
++m_statistics.operatorBindings;
}
void Mult(
const mfem::Vector &normalizedResidual,
mfem::Vector &normalizedCorrection
) const override {
if (m_normalizedJacobian == nullptr) {
throw std::logic_error("The scaled preconditioner has not been bound to a normalized Jacobian.");
}
if (normalizedResidual.Size() != Width() || normalizedCorrection.Size() != Height()) {
throw std::invalid_argument(
"The scaled preconditioner requires compatible, preallocated normalized vectors."
);
}
m_normalization->DenormalizeResidual(normalizedResidual, m_physicalResidual);
m_physicalInverse->Mult(m_physicalResidual, m_physicalCorrection);
m_normalization->NormalizeState(m_physicalCorrection, normalizedCorrection);
++m_statistics.applications;
}
[[nodiscard]] const mfem::Solver &GetPhysicalInverse() const noexcept {
return *m_physicalInverse;
}
[[nodiscard]] const mfem::Operator &GetPhysicalJacobian() const noexcept {
return *m_physicalJacobian;
}
[[nodiscard]] const mfem::Operator &GetNormalizedJacobian() const {
if (m_normalizedJacobian == nullptr) {
throw std::logic_error("The scaled preconditioner has not been bound to a normalized Jacobian.");
}
return *m_normalizedJacobian;
}
[[nodiscard]] const ScaledPreconditionerStatistics &GetStatistics() const noexcept {
return m_statistics;
}
private:
mfem::Solver *m_physicalInverse;
const mfem::Operator *m_physicalJacobian;
const mfem::Operator *m_expectedNormalizedJacobian;
const mfem::Operator *m_normalizedJacobian{nullptr};
const DiagonalNormalization *m_normalization;
mutable mfem::Vector m_physicalResidual;
mutable mfem::Vector m_physicalCorrection;
mutable ScaledPreconditionerStatistics m_statistics;
};
} // namespace mean_field::normalization

View File

@@ -0,0 +1,728 @@
module;
#include <cmath>
#include <concepts>
#include <stdexcept>
#include <type_traits>
export module mean_field:normalization.physical_riesz;
export import :dimensions.quantities;
export import :field.mfem;
export import :model.specifications;
export import :normalization.plan;
export namespace mean_field::normalization {
struct Unnormalized final : NormalizationPrescriptionTag { };
struct ReferenceGeometry final { };
struct FixedMassBranchReference final { };
template <typename Candidate>
concept RieszGeometryPolicy = std::same_as<std::remove_cvref_t<Candidate>, ReferenceGeometry>;
template <typename Candidate>
concept ReferenceScalePolicy = std::same_as<std::remove_cvref_t<Candidate>, FixedMassBranchReference>;
template <
RieszGeometryPolicy GeometryPolicy = ReferenceGeometry,
ReferenceScalePolicy ScalePolicy = FixedMassBranchReference>
class PhysicalRieszDiagonal final : public NormalizationPrescriptionTag {
public:
using Geometry = GeometryPolicy;
using ScaleSource = ScalePolicy;
explicit PhysicalRieszDiagonal(
const dimensions::LengthValue referenceRadius,
const double gravitationalConstant = 1.0
)
: m_referenceRadius(referenceRadius),
m_gravitationalConstant(gravitationalConstant) {
if (!std::isfinite(referenceRadius.value()) || referenceRadius.value() <= 0.0) {
throw std::invalid_argument("Physical Riesz normalization requires a finite, positive branch radius.");
}
if (!std::isfinite(gravitationalConstant) || gravitationalConstant <= 0.0) {
throw std::invalid_argument(
"Physical Riesz normalization requires a finite, positive gravitational constant."
);
}
}
[[nodiscard]] dimensions::LengthValue referenceRadius() const noexcept {
return m_referenceRadius;
}
[[nodiscard]] double gravitationalConstant() const noexcept {
return m_gravitationalConstant;
}
private:
dimensions::LengthValue m_referenceRadius;
double m_gravitationalConstant;
};
PhysicalRieszDiagonal(dimensions::LengthValue, double = 1.0)
-> PhysicalRieszDiagonal<ReferenceGeometry, FixedMassBranchReference>;
template <typename Candidate> struct IsPhysicalRieszDiagonal : std::false_type { };
template <RieszGeometryPolicy Geometry, ReferenceScalePolicy ScaleSource>
struct IsPhysicalRieszDiagonal<PhysicalRieszDiagonal<Geometry, ScaleSource>> : std::true_type { };
template <typename Candidate>
concept PhysicalRieszDiagonalPrescription =
IsPhysicalRieszDiagonal<std::remove_cvref_t<Candidate>>::value;
struct StellarCharacteristicScales final {
dimensions::MassValue mass;
dimensions::LengthValue radius;
double gravitationalConstant;
double density;
double acceleration;
double inverseTimeSquared;
double specificEnergy;
double pressure;
double angularVelocity;
double angularMomentum;
double force;
};
[[nodiscard]] inline StellarCharacteristicScales deriveStellarCharacteristicScales(
const dimensions::MassValue mass,
const dimensions::LengthValue radius,
const double gravitationalConstant = 1.0
) {
const double massValue = mass.value();
const double radiusValue = radius.value();
if (!std::isfinite(massValue) || massValue <= 0.0) {
throw std::invalid_argument("Characteristic stellar scales require a finite, positive mass.");
}
if (!std::isfinite(radiusValue) || radiusValue <= 0.0) {
throw std::invalid_argument("Characteristic stellar scales require a finite, positive radius.");
}
if (!std::isfinite(gravitationalConstant) || gravitationalConstant <= 0.0) {
throw std::invalid_argument(
"Characteristic stellar scales require a finite, positive gravitational constant."
);
}
const double radiusSquared = radiusValue * radiusValue;
const double radiusCubed = radiusSquared * radiusValue;
const double density = massValue / radiusCubed;
const double acceleration = gravitationalConstant * massValue / radiusSquared;
const double inverseTimeSquared = gravitationalConstant * massValue / radiusCubed;
const double specificEnergy = gravitationalConstant * massValue / radiusValue;
const double pressure = gravitationalConstant * massValue * massValue /
(radiusSquared * radiusSquared);
const double angularVelocity = std::sqrt(inverseTimeSquared);
const double angularMomentum = massValue * std::sqrt(gravitationalConstant * massValue * radiusValue);
const double force = gravitationalConstant * massValue * massValue / radiusSquared;
const double derived[] = {
density,
acceleration,
inverseTimeSquared,
specificEnergy,
pressure,
angularVelocity,
angularMomentum,
force
};
for (const double value : derived) {
if (!std::isfinite(value) || value <= 0.0) {
throw std::overflow_error("A derived characteristic stellar scale is not finite and positive.");
}
}
return {
.mass = mass,
.radius = radius,
.gravitationalConstant = gravitationalConstant,
.density = density,
.acceleration = acceleration,
.inverseTimeSquared = inverseTimeSquared,
.specificEnergy = specificEnergy,
.pressure = pressure,
.angularVelocity = angularVelocity,
.angularMomentum = angularMomentum,
.force = force
};
}
template <RieszGeometryPolicy Geometry, ReferenceScalePolicy ScaleSource, typename Model>
requires requires(const Model &model) {
{
model.template specification<models::FixedTotalMass>()
} -> std::same_as<const models::FixedTotalMass &>;
{
model.template specification<models::FixedTotalMass>().targetMass()
} -> std::same_as<dimensions::MassValue>;
}
[[nodiscard]] StellarCharacteristicScales deriveStellarCharacteristicScales(
const PhysicalRieszDiagonal<Geometry, ScaleSource> &prescription,
const Model &model
) {
return deriveStellarCharacteristicScales(
model.template specification<models::FixedTotalMass>().targetMass(),
prescription.referenceRadius(),
prescription.gravitationalConstant()
);
}
namespace detail {
/*
* Model definitions live below the numerical normalization layer so
* that a physics component can describe its generated coordinates
* without importing solver machinery. These two translations are the
* deliberately small boundary between that neutral declaration and the
* normalization plan used by the discretization.
*/
template <models::RieszTopology Topology> struct DeclaredRieszTopology {
static constexpr bool available = false;
static constexpr RieszTopology value = RieszTopology::identity;
};
#define MEAN_FIELD_DECLARED_RIESZ_TOPOLOGY(Name) \
template <> struct DeclaredRieszTopology<models::RieszTopology::Name> { \
static constexpr bool available = true; \
static constexpr RieszTopology value = RieszTopology::Name; \
}
MEAN_FIELD_DECLARED_RIESZ_TOPOLOGY(identity);
MEAN_FIELD_DECLARED_RIESZ_TOPOLOGY(scalar_volume_l2);
MEAN_FIELD_DECLARED_RIESZ_TOPOLOGY(vector_volume_l2);
MEAN_FIELD_DECLARED_RIESZ_TOPOLOGY(scalar_boundary_l2);
MEAN_FIELD_DECLARED_RIESZ_TOPOLOGY(hybrid_scalar_volume_point_rows);
MEAN_FIELD_DECLARED_RIESZ_TOPOLOGY(global_scalar);
#undef MEAN_FIELD_DECLARED_RIESZ_TOPOLOGY
template <models::PhysicalScaleLaw Scale> struct DeclaredPhysicalScale {
static constexpr bool available = false;
static constexpr PhysicalScaleKind value = PhysicalScaleKind::dimensionless;
};
#define MEAN_FIELD_DECLARED_PHYSICAL_SCALE(Name) \
template <> struct DeclaredPhysicalScale<models::PhysicalScaleLaw::Name> { \
static constexpr bool available = true; \
static constexpr PhysicalScaleKind value = PhysicalScaleKind::Name; \
}
MEAN_FIELD_DECLARED_PHYSICAL_SCALE(dimensionless);
MEAN_FIELD_DECLARED_PHYSICAL_SCALE(density);
MEAN_FIELD_DECLARED_PHYSICAL_SCALE(length);
MEAN_FIELD_DECLARED_PHYSICAL_SCALE(acceleration);
MEAN_FIELD_DECLARED_PHYSICAL_SCALE(inverse_time_squared);
MEAN_FIELD_DECLARED_PHYSICAL_SCALE(specific_energy);
MEAN_FIELD_DECLARED_PHYSICAL_SCALE(pressure);
MEAN_FIELD_DECLARED_PHYSICAL_SCALE(mass);
MEAN_FIELD_DECLARED_PHYSICAL_SCALE(force);
MEAN_FIELD_DECLARED_PHYSICAL_SCALE(angular_velocity);
MEAN_FIELD_DECLARED_PHYSICAL_SCALE(angular_momentum);
#undef MEAN_FIELD_DECLARED_PHYSICAL_SCALE
template <typename Declaration, typename = void>
struct CompileDeclaredPhysicalRieszCoordinate {
using Method = UnsupportedPhysicalRieszCoordinate;
static constexpr bool registered = false;
};
template <typename Declaration>
struct CompileDeclaredPhysicalRieszCoordinate<
Declaration,
std::void_t<
decltype(std::integral_constant<
models::RieszTopology,
static_cast<models::RieszTopology>(Declaration::topology)>{}),
decltype(std::integral_constant<
models::PhysicalScaleLaw,
static_cast<models::PhysicalScaleLaw>(Declaration::scale)>{}),
decltype(std::bool_constant<static_cast<bool>(Declaration::available)>{})>> {
private:
static constexpr models::RieszTopology declaredTopology =
static_cast<models::RieszTopology>(Declaration::topology);
static constexpr models::PhysicalScaleLaw declaredScale =
static_cast<models::PhysicalScaleLaw>(Declaration::scale);
using Topology = DeclaredRieszTopology<declaredTopology>;
using Scale = DeclaredPhysicalScale<declaredScale>;
public:
static constexpr bool registered = static_cast<bool>(Declaration::available) &&
Topology::available && Scale::available;
using Method = std::conditional_t<
registered,
PhysicalRieszCoordinate<Topology::value, Scale::value>,
UnsupportedPhysicalRieszCoordinate>;
};
template <typename Generated, CoordinateKind Kind, typename = void>
struct DeclaredGeneratedPhysicalRieszCoordinate {
using Method = UnsupportedPhysicalRieszCoordinate;
static constexpr bool registered = false;
};
template <typename Generated>
struct DeclaredGeneratedPhysicalRieszCoordinate<
Generated,
CoordinateKind::value,
std::void_t<
typename Generated::SpecificationType,
typename models::SpecificationContribution<
typename Generated::SpecificationType>::Normalization::Value>>
: CompileDeclaredPhysicalRieszCoordinate<
typename models::SpecificationContribution<
typename Generated::SpecificationType>::Normalization::Value> { };
template <typename Generated>
struct DeclaredGeneratedPhysicalRieszCoordinate<
Generated,
CoordinateKind::residual,
std::void_t<
typename Generated::SpecificationType,
typename models::SpecificationContribution<
typename Generated::SpecificationType>::Normalization::Residual>>
: CompileDeclaredPhysicalRieszCoordinate<
typename models::SpecificationContribution<
typename Generated::SpecificationType>::Normalization::Residual> { };
template <typename GeneratedValues, typename GeneratedResiduals>
struct GeneratedPhysicalRieszCoverage {
static constexpr bool complete = false;
};
template <typename... GeneratedValues, typename... GeneratedResiduals>
struct GeneratedPhysicalRieszCoverage<
models::ModelTypeList<GeneratedValues...>,
models::ModelTypeList<GeneratedResiduals...>> {
static constexpr bool complete =
(DeclaredGeneratedPhysicalRieszCoordinate<
GeneratedValues,
CoordinateKind::value>::registered && ...) &&
(DeclaredGeneratedPhysicalRieszCoordinate<
GeneratedResiduals,
CoordinateKind::residual>::registered && ...);
};
template <typename Specification, typename = void>
struct SpecificationPhysicalRieszCoverage {
static constexpr bool complete = false;
};
template <models::ModelSpecification Specification>
struct SpecificationPhysicalRieszCoverage<
Specification,
std::void_t<
typename models::SpecificationContribution<Specification>::GeneratedValues,
typename models::SpecificationContribution<Specification>::GeneratedResiduals>>
: GeneratedPhysicalRieszCoverage<
typename models::SpecificationContribution<Specification>::GeneratedValues,
typename models::SpecificationContribution<Specification>::GeneratedResiduals> { };
} // namespace detail
/*
* All generated blocks are normalized from their generating physics
* specification. Adding another constraint therefore does not add a
* normalization specialization: its public ModelDefinition is the single
* source of both the value and residual Riesz laws.
*/
template <typename Generated>
struct PhysicalRieszBlockTraits<utils::blocks::generated_value_block<Generated>>
: detail::DeclaredGeneratedPhysicalRieszCoordinate<Generated, CoordinateKind::value> { };
template <typename Generated>
struct PhysicalRieszBlockTraits<utils::blocks::generated_residual_block<Generated>>
: detail::DeclaredGeneratedPhysicalRieszCoordinate<Generated, CoordinateKind::residual> { };
template <typename Generated>
concept GeneratedValuePhysicalRieszNormalizable =
detail::DeclaredGeneratedPhysicalRieszCoordinate<Generated, CoordinateKind::value>::registered;
template <typename Generated>
concept GeneratedResidualPhysicalRieszNormalizable =
detail::DeclaredGeneratedPhysicalRieszCoordinate<Generated, CoordinateKind::residual>::registered;
template <typename Specification>
concept CompleteGeneratedPhysicalRieszNormalizationFor =
detail::SpecificationPhysicalRieszCoverage<std::remove_cvref_t<Specification>>::complete;
/*
* Runtime Physical Riesz assembly needs more than a symbolically complete
* plan: it must be able to recover the finite-element maps owned by the
* selected physical core. Keep that structural capability in this low
* normalization module so both problem formation and the solver-facing
* adapter can consult the same authority without importing one another.
*/
template <typename Candidate>
concept PhysicalRieszCoreRuntime =
requires(const std::remove_cvref_t<Candidate> &core) {
{
core.GetGravityContext().GetDensityMap()
} -> std::same_as<const field::FieldDofMap &>;
{
core.GetGravityContext().GetGravityGradientMap()
} -> std::same_as<const field::FieldDofMap &>;
{
core.GetGravityContext().GetGravityPotentialMap()
} -> std::same_as<const field::FieldDofMap &>;
{
core.GetHydrostaticOperator().GetEnthalpyMap()
} -> std::same_as<const field::FieldDofMap &>;
{
core.GetDomainDeformation().parameterCount()
} -> std::same_as<int>;
};
namespace detail {
template <typename Generated, CoordinateKind Kind>
using GeneratedPhysicalRieszMethod =
typename DeclaredGeneratedPhysicalRieszCoordinate<Generated, Kind>::Method;
template <typename Generated, CoordinateKind Kind, typename = void>
struct GeneratedPhysicalRieszRuntimeCoordinate : std::false_type { };
template <typename Generated, CoordinateKind Kind>
struct GeneratedPhysicalRieszRuntimeCoordinate<
Generated,
Kind,
std::void_t<decltype(GeneratedPhysicalRieszMethod<Generated, Kind>::topology)>>
: std::bool_constant<
DeclaredGeneratedPhysicalRieszCoordinate<Generated, Kind>::registered &&
GeneratedPhysicalRieszMethod<Generated, Kind>::topology ==
RieszTopology::global_scalar> { };
template <typename Specification, typename = void>
struct SpecificationPhysicalRieszRuntimeCoverage : std::false_type { };
template <typename Values, typename Residuals>
struct GeneratedPhysicalRieszRuntimeCoverage : std::false_type { };
template <typename... Values, typename... Residuals>
struct GeneratedPhysicalRieszRuntimeCoverage<
models::ModelTypeList<Values...>,
models::ModelTypeList<Residuals...>>
: std::bool_constant<
(GeneratedPhysicalRieszRuntimeCoordinate<Values, CoordinateKind::value>::value && ...) &&
(GeneratedPhysicalRieszRuntimeCoordinate<Residuals, CoordinateKind::residual>::value && ...)> { };
template <models::ModelSpecification Specification>
struct SpecificationPhysicalRieszRuntimeCoverage<
Specification,
std::void_t<
typename models::SpecificationContribution<Specification>::GeneratedValues,
typename models::SpecificationContribution<Specification>::GeneratedResiduals>>
: GeneratedPhysicalRieszRuntimeCoverage<
typename models::SpecificationContribution<Specification>::GeneratedValues,
typename models::SpecificationContribution<Specification>::GeneratedResiduals> { };
template <typename SpecificationTypes>
struct SpecificationSetPhysicalRieszRuntimeCoverage : std::false_type { };
template <models::ModelSpecification... Specifications>
struct SpecificationSetPhysicalRieszRuntimeCoverage<
models::detail::SpecificationSetStorage<Specifications...>>
: std::bool_constant<
(SpecificationPhysicalRieszRuntimeCoverage<Specifications>::value && ...)> { };
} // namespace detail
template <typename Specification>
concept CompleteGeneratedPhysicalRieszRuntimeNormalizationFor =
detail::SpecificationPhysicalRieszRuntimeCoverage<
std::remove_cvref_t<Specification>>::value;
#define MEAN_FIELD_PHYSICAL_RIESZ_TRAIT(BlockType, TopologyValue, ScaleValue) \
template <> struct PhysicalRieszBlockTraits<BlockType> { \
using Method = PhysicalRieszCoordinate<RieszTopology::TopologyValue, PhysicalScaleKind::ScaleValue>; \
static constexpr bool registered = true; \
}
MEAN_FIELD_PHYSICAL_RIESZ_TRAIT(
utils::blocks::density::mass::value,
scalar_volume_l2,
density
);
MEAN_FIELD_PHYSICAL_RIESZ_TRAIT(
utils::blocks::surface_deformation::parameters::value,
scalar_boundary_l2,
length
);
MEAN_FIELD_PHYSICAL_RIESZ_TRAIT(
utils::blocks::gravity::gradient::value,
vector_volume_l2,
acceleration
);
MEAN_FIELD_PHYSICAL_RIESZ_TRAIT(
utils::blocks::gravity::poisson::value,
scalar_volume_l2,
specific_energy
);
MEAN_FIELD_PHYSICAL_RIESZ_TRAIT(
utils::blocks::enthalpy::specific::value,
scalar_volume_l2,
specific_energy
);
MEAN_FIELD_PHYSICAL_RIESZ_TRAIT(
utils::blocks::gravity::gradient::residual,
vector_volume_l2,
acceleration
);
MEAN_FIELD_PHYSICAL_RIESZ_TRAIT(
utils::blocks::gravity::poisson::residual,
scalar_volume_l2,
inverse_time_squared
);
MEAN_FIELD_PHYSICAL_RIESZ_TRAIT(
utils::blocks::density::mass::residual,
scalar_volume_l2,
density
);
MEAN_FIELD_PHYSICAL_RIESZ_TRAIT(
utils::blocks::surface_deformation::shape_equilibrium::residual,
scalar_boundary_l2,
force
);
MEAN_FIELD_PHYSICAL_RIESZ_TRAIT(
utils::blocks::enthalpy::specific::residual,
hybrid_scalar_volume_point_rows,
specific_energy
);
#undef MEAN_FIELD_PHYSICAL_RIESZ_TRAIT
template <typename Block>
[[nodiscard]] double physicalScale(
const StellarCharacteristicScales &scales
) {
static_assert(PhysicalRieszBlockTraits<Block>::registered, "The block has no Physical Riesz normalization.");
using Method = typename PhysicalRieszBlockTraits<Block>::Method;
constexpr PhysicalScaleKind scale = Method::scale;
if constexpr (scale == PhysicalScaleKind::dimensionless) {
return 1.0;
} else if constexpr (scale == PhysicalScaleKind::density) {
return scales.density;
} else if constexpr (scale == PhysicalScaleKind::length) {
return scales.radius.value();
} else if constexpr (scale == PhysicalScaleKind::acceleration) {
return scales.acceleration;
} else if constexpr (scale == PhysicalScaleKind::inverse_time_squared) {
return scales.inverseTimeSquared;
} else if constexpr (scale == PhysicalScaleKind::specific_energy) {
return scales.specificEnergy;
} else if constexpr (scale == PhysicalScaleKind::pressure) {
return scales.pressure;
} else if constexpr (scale == PhysicalScaleKind::mass) {
return scales.mass.value();
} else if constexpr (scale == PhysicalScaleKind::force) {
return scales.force;
} else if constexpr (scale == PhysicalScaleKind::angular_velocity) {
return scales.angularVelocity;
} else {
static_assert(scale == PhysicalScaleKind::angular_momentum);
return scales.angularMomentum;
}
}
namespace detail {
template <typename Values, typename Residuals> struct MakePhysicalRieszPlan;
template <typename... Values, typename... Residuals>
struct MakePhysicalRieszPlan<
utils::blocks::type_list<Values...>,
utils::blocks::type_list<Residuals...>> {
using Type = NormalizationPlan<
CoordinateComponent<
CoordinateKind::value,
utils::blocks::type_list<Values>,
typename PhysicalRieszBlockTraits<Values>::Method>...,
CoordinateComponent<
CoordinateKind::residual,
utils::blocks::type_list<Residuals>,
typename PhysicalRieszBlockTraits<Residuals>::Method>...>;
};
} // namespace detail
template <typename Form>
requires utils::blocks::block_form_is_valid_v<Form>
using PhysicalRieszNormalizationPlanFor = typename detail::MakePhysicalRieszPlan<
typename Form::value_blocks,
typename Form::residual_blocks>::Type;
/*
* Public compile-time extension point for a normalization prescription.
* A specialization owns both the complete coordinate plan and the
* low-level runtime compatibility predicate used before a discretized
* problem type is formed. Keeping those declarations together prevents a
* policy from compiling a plan which the selected stellar core cannot
* actually prepare.
*/
template <typename Prescription, typename Form> struct NormalizationCompilation {
using Plan = NormalizationPlan<>;
static constexpr bool registered = false;
template <typename PhysicalCore, typename SpecificationTypes>
static constexpr bool runtimeAvailableFor = false;
};
/* Astronomy/numerics-facing package for a policy which prepares one
* runtime diagonal over the complete inferred form and needs no private
* facility of a particular stellar core. The generated plan truthfully
* labels every coordinate as runtime-prepared by this exact policy. */
template <NormalizationPrescription Prescription, typename Form>
requires utils::blocks::block_form_is_valid_v<Form>
struct RuntimePreparedNormalizationCompilation {
using Plan = RuntimePreparedNormalizationPlanFor<Prescription, Form>;
static constexpr bool registered = CompleteNormalizationFor<Plan, Form>;
template <typename PhysicalCore, typename SpecificationTypes>
static constexpr bool runtimeAvailableFor = registered;
};
template <typename Form>
requires utils::blocks::block_form_is_valid_v<Form>
struct NormalizationCompilation<Unnormalized, Form> {
using Plan = IdentityNormalizationPlanFor<Form>;
static constexpr bool registered = CompleteNormalizationFor<Plan, Form>;
template <typename PhysicalCore, typename SpecificationTypes>
static constexpr bool runtimeAvailableFor = registered;
};
template <RieszGeometryPolicy Geometry, ReferenceScalePolicy ScaleSource, typename Form>
requires utils::blocks::block_form_is_valid_v<Form>
struct NormalizationCompilation<PhysicalRieszDiagonal<Geometry, ScaleSource>, Form> {
using Plan = PhysicalRieszNormalizationPlanFor<Form>;
static constexpr bool registered = CompleteNormalizationFor<Plan, Form>;
template <typename PhysicalCore, typename SpecificationTypes>
static constexpr bool runtimeAvailableFor =
registered &&
PhysicalRieszCoreRuntime<std::remove_cvref_t<PhysicalCore>> &&
detail::SpecificationSetPhysicalRieszRuntimeCoverage<
std::remove_cvref_t<SpecificationTypes>>::value;
};
namespace detail {
template <typename Prescription, typename Form, typename = void>
struct NormalizationCompilationAudit {
using Plan = NormalizationPlan<>;
static constexpr bool registered = false;
};
template <typename Prescription, typename Form>
requires NormalizationPrescription<std::remove_cvref_t<Prescription>> &&
utils::blocks::block_form_is_valid_v<std::remove_cvref_t<Form>>
struct NormalizationCompilationAudit<
Prescription,
Form,
std::void_t<
typename NormalizationCompilation<
std::remove_cvref_t<Prescription>,
std::remove_cvref_t<Form>>::Plan,
decltype(std::bool_constant<static_cast<bool>(
NormalizationCompilation<
std::remove_cvref_t<Prescription>,
std::remove_cvref_t<Form>>::registered)>{})>> {
using Compilation = NormalizationCompilation<
std::remove_cvref_t<Prescription>,
std::remove_cvref_t<Form>>;
using Plan = typename Compilation::Plan;
static constexpr bool registered =
static_cast<bool>(Compilation::registered) &&
CompleteNormalizationFor<Plan, std::remove_cvref_t<Form>>;
};
} // namespace detail
template <NormalizationPrescription Prescription, typename Form>
using NormalizationPlanFor = typename detail::NormalizationCompilationAudit<
std::remove_cvref_t<Prescription>,
Form>::Plan;
template <typename Prescription, typename Form>
concept CompilableNormalizationFor =
detail::NormalizationCompilationAudit<
std::remove_cvref_t<Prescription>,
std::remove_cvref_t<Form>>::registered;
/* The public runtime-preparation adapter is intentionally narrower than
* an arbitrary complete plan: every coordinate must name the exact policy
* which supplies its runtime factor. This prevents a custom policy from
* advertising IdentityCoordinate (or another policy's method) while
* silently installing a different diagonal at runtime. */
template <typename Prescription, typename Form>
concept RuntimePreparedNormalizationFor =
NormalizationPrescription<std::remove_cvref_t<Prescription>> &&
utils::blocks::block_form_is_valid_v<std::remove_cvref_t<Form>> &&
CompilableNormalizationFor<
std::remove_cvref_t<Prescription>,
std::remove_cvref_t<Form>> &&
std::same_as<
NormalizationPlanFor<
std::remove_cvref_t<Prescription>,
std::remove_cvref_t<Form>>,
RuntimePreparedNormalizationPlanFor<
std::remove_cvref_t<Prescription>,
std::remove_cvref_t<Form>>>;
namespace detail {
template <
typename Prescription,
typename Form,
typename PhysicalCore,
typename SpecificationTypes,
typename = void>
struct StellarNormalizationRuntimeAudit : std::false_type { };
template <
typename Prescription,
typename Form,
typename PhysicalCore,
typename SpecificationTypes>
struct StellarNormalizationRuntimeAudit<
Prescription,
Form,
PhysicalCore,
SpecificationTypes,
std::void_t<
std::enable_if_t<NormalizationCompilationAudit<
Prescription,
Form>::registered>,
decltype(std::bool_constant<static_cast<bool>(
NormalizationCompilation<
Prescription,
Form>::template runtimeAvailableFor<
PhysicalCore,
SpecificationTypes>)>{})>>
: std::bool_constant<
(std::same_as<Prescription, Unnormalized> ||
PhysicalRieszDiagonalPrescription<Prescription> ||
RuntimePreparedNormalizationFor<Prescription, Form>) &&
static_cast<bool>(NormalizationCompilation<
Prescription,
Form>::template runtimeAvailableFor<
PhysicalCore,
SpecificationTypes>)> { };
} // namespace detail
/*
* Single detection-safe authority for pairing a compiled stellar form,
* its selected physical core, and a runtime normalization prescription.
* Each public NormalizationCompilation specialization declares this
* compatibility alongside its plan. The identity policy needs only a
* complete plan. Physical Riesz also requires every map consumed during
* assembly and global-scalar runtime preparation for every generated
* coordinate in the specification pack.
*/
template <
typename Prescription,
typename Form,
typename PhysicalCore,
typename SpecificationTypes>
concept StellarNormalizationRuntimeAvailableFor =
detail::StellarNormalizationRuntimeAudit<
std::remove_cvref_t<Prescription>,
std::remove_cvref_t<Form>,
std::remove_cvref_t<PhysicalCore>,
std::remove_cvref_t<SpecificationTypes>>::value;
} // namespace mean_field::normalization

View File

@@ -0,0 +1,376 @@
module;
#include <concepts>
#include <type_traits>
export module mean_field:normalization.plan;
export import :utils.blocks;
export namespace mean_field::normalization {
struct NormalizationPrescriptionTag { };
template <typename Candidate>
concept NormalizationPrescription =
std::derived_from<
std::remove_cvref_t<Candidate>,
NormalizationPrescriptionTag>;
enum class CoordinateKind { value, residual };
enum class RieszTopology {
identity,
scalar_volume_l2,
vector_volume_l2,
scalar_boundary_l2,
hybrid_scalar_volume_point_rows,
global_scalar
};
enum class PhysicalScaleKind {
dimensionless,
density,
length,
acceleration,
inverse_time_squared,
specific_energy,
pressure,
mass,
force,
angular_velocity,
angular_momentum
};
struct IdentityCoordinate final { };
/*
* Honest compile-time method for a coordinate whose positive diagonal
* factor is supplied at runtime by one exact normalization prescription.
* Unlike IdentityCoordinate, this category makes no claim about the
* numerical value of that factor. The owner type prevents one policy from
* silently presenting another policy's runtime map as its own plan.
*/
template <NormalizationPrescription Prescription>
struct RuntimePreparedCoordinate final {
using PrescriptionType = std::remove_cvref_t<Prescription>;
};
template <RieszTopology Topology, PhysicalScaleKind Scale> struct PhysicalRieszCoordinate final {
static constexpr RieszTopology topology = Topology;
static constexpr PhysicalScaleKind scale = Scale;
};
struct UnsupportedPhysicalRieszCoordinate final { };
template <typename Block> struct PhysicalRieszBlockTraits {
using Method = UnsupportedPhysicalRieszCoordinate;
static constexpr bool registered = false;
};
template <CoordinateKind Kind, typename BlockList, typename MethodType>
struct CoordinateComponent final {
using Blocks = BlockList;
using Method = MethodType;
static constexpr CoordinateKind kind = Kind;
using ValueBlocks = std::conditional_t<
Kind == CoordinateKind::value,
BlockList,
utils::blocks::type_list<>>;
using ResidualBlocks = std::conditional_t<
Kind == CoordinateKind::residual,
BlockList,
utils::blocks::type_list<>>;
};
namespace detail {
template <typename Candidate> struct IsTypeList : std::false_type { };
template <typename... Types>
struct IsTypeList<utils::blocks::type_list<Types...>> : std::true_type { };
template <typename List, typename Base> struct IsUniqueDerivedBlockList : std::false_type { };
template <typename Base, typename... Blocks>
struct IsUniqueDerivedBlockList<utils::blocks::type_list<Blocks...>, Base>
: std::bool_constant<
(std::derived_from<Blocks, Base> && ...) &&
utils::blocks::types_are_unique_v<utils::blocks::type_list<Blocks...>>> { };
template <typename Method> struct IsCoordinateMethod : std::false_type { };
template <> struct IsCoordinateMethod<IdentityCoordinate> : std::true_type { };
template <NormalizationPrescription Prescription>
struct IsCoordinateMethod<RuntimePreparedCoordinate<Prescription>>
: std::true_type { };
template <RieszTopology Topology, PhysicalScaleKind Scale>
struct IsCoordinateMethod<PhysicalRieszCoordinate<Topology, Scale>> : std::true_type { };
template <typename Method, typename Block> struct MethodSupportsBlock : std::false_type { };
template <typename Block>
struct MethodSupportsBlock<IdentityCoordinate, Block>
: std::bool_constant<std::derived_from<Block, utils::blocks::block>> { };
template <NormalizationPrescription Prescription, typename Block>
struct MethodSupportsBlock<RuntimePreparedCoordinate<Prescription>, Block>
: std::bool_constant<std::derived_from<Block, utils::blocks::block>> { };
template <RieszTopology Topology, PhysicalScaleKind Scale, typename Block>
struct MethodSupportsBlock<PhysicalRieszCoordinate<Topology, Scale>, Block>
: std::bool_constant<
PhysicalRieszBlockTraits<Block>::registered &&
std::same_as<
typename PhysicalRieszBlockTraits<Block>::Method,
PhysicalRieszCoordinate<Topology, Scale>>> { };
template <typename Method, typename List> struct MethodSupportsEveryBlock : std::false_type { };
template <typename Method, typename... Blocks>
struct MethodSupportsEveryBlock<Method, utils::blocks::type_list<Blocks...>>
: std::bool_constant<(MethodSupportsBlock<Method, Blocks>::value && ...)> { };
template <typename Candidate, typename = void> struct ComponentTraits {
static constexpr bool valid = false;
};
template <typename Candidate>
struct ComponentTraits<
Candidate,
std::void_t<
typename Candidate::Blocks,
typename Candidate::Method,
typename Candidate::ValueBlocks,
typename Candidate::ResidualBlocks,
decltype(Candidate::kind)>> {
using Blocks = typename Candidate::Blocks;
using Method = typename Candidate::Method;
using ValueBlocks = typename Candidate::ValueBlocks;
using ResidualBlocks = typename Candidate::ResidualBlocks;
static constexpr bool hasValidKind =
std::same_as<std::remove_cv_t<decltype(Candidate::kind)>, CoordinateKind>;
static constexpr bool hasValidBlockList = [] {
if constexpr (!hasValidKind || !IsTypeList<Blocks>::value) {
return false;
} else if constexpr (Candidate::kind == CoordinateKind::value) {
return IsUniqueDerivedBlockList<Blocks, utils::blocks::value_block_base>::value;
} else if constexpr (Candidate::kind == CoordinateKind::residual) {
return IsUniqueDerivedBlockList<Blocks, utils::blocks::residual_block_base>::value;
} else {
return false;
}
}();
static constexpr bool hasCoherentCoordinateLists = [] {
if constexpr (!hasValidKind || !IsTypeList<ValueBlocks>::value ||
!IsTypeList<ResidualBlocks>::value) {
return false;
} else if constexpr (Candidate::kind == CoordinateKind::value) {
return std::same_as<ValueBlocks, Blocks> &&
std::same_as<ResidualBlocks, utils::blocks::type_list<>>;
} else if constexpr (Candidate::kind == CoordinateKind::residual) {
return std::same_as<ValueBlocks, utils::blocks::type_list<>> &&
std::same_as<ResidualBlocks, Blocks>;
} else {
return false;
}
}();
static constexpr bool valid = hasValidKind && IsTypeList<Blocks>::value &&
IsCoordinateMethod<Method>::value && hasValidBlockList &&
hasCoherentCoordinateLists &&
MethodSupportsEveryBlock<Method, Blocks>::value;
};
template <typename... Lists> struct Concatenate;
template <> struct Concatenate<> {
using Type = utils::blocks::type_list<>;
};
template <typename... Types> struct Concatenate<utils::blocks::type_list<Types...>> {
using Type = utils::blocks::type_list<Types...>;
};
template <typename... Left, typename... Right, typename... Remaining>
struct Concatenate<utils::blocks::type_list<Left...>, utils::blocks::type_list<Right...>, Remaining...> {
using Type = typename Concatenate<utils::blocks::type_list<Left..., Right...>, Remaining...>::Type;
};
template <typename... Lists> using ConcatenateT = typename Concatenate<Lists...>::Type;
template <typename List, typename Type> struct Append;
template <typename... Types, typename Appended>
struct Append<utils::blocks::type_list<Types...>, Appended> {
using Type = utils::blocks::type_list<Types..., Appended>;
};
template <typename List, typename Type> using AppendT = typename Append<List, Type>::Type;
template <typename List, typename Type>
using AppendUniqueT = std::conditional_t<
utils::blocks::contains_type_v<Type, List>,
List,
AppendT<List, Type>>;
template <typename Source, typename Excluded> struct ListDifference;
template <typename Excluded>
struct ListDifference<utils::blocks::type_list<>, Excluded> {
using Type = utils::blocks::type_list<>;
};
template <typename Head, typename... Tail, typename Excluded>
struct ListDifference<utils::blocks::type_list<Head, Tail...>, Excluded> {
private:
using Remaining = typename ListDifference<utils::blocks::type_list<Tail...>, Excluded>::Type;
public:
using Type = std::conditional_t<
utils::blocks::contains_type_v<Head, Excluded>,
Remaining,
ConcatenateT<utils::blocks::type_list<Head>, Remaining>>;
};
template <typename Source, typename Excluded>
using ListDifferenceT = typename ListDifference<Source, Excluded>::Type;
template <typename Remaining, typename Original, typename Repeated> struct CollectRepeatedTypes;
template <typename Original, typename Repeated>
struct CollectRepeatedTypes<utils::blocks::type_list<>, Original, Repeated> {
using Type = Repeated;
};
template <typename Head, typename... Tail, typename Original, typename Repeated>
struct CollectRepeatedTypes<utils::blocks::type_list<Head, Tail...>, Original, Repeated> {
private:
using Next = std::conditional_t<
(utils::blocks::type_count_v<Head, Original> > 1),
AppendUniqueT<Repeated, Head>,
Repeated>;
public:
using Type = typename CollectRepeatedTypes<utils::blocks::type_list<Tail...>, Original, Next>::Type;
};
template <typename List>
using RepeatedTypesT = typename CollectRepeatedTypes<
List,
List,
utils::blocks::type_list<>>::Type;
template <typename Candidate, typename = void> struct PlanTraits {
static constexpr bool valid = false;
};
} // namespace detail
template <typename Candidate>
concept NormalizationComponent = detail::ComponentTraits<std::remove_cvref_t<Candidate>>::valid;
template <typename... Components> struct NormalizationPlan final {
using ComponentTypes = utils::blocks::type_list<Components...>;
using ValueBlocks = detail::ConcatenateT<typename Components::ValueBlocks...>;
using ResidualBlocks = detail::ConcatenateT<typename Components::ResidualBlocks...>;
};
namespace detail {
template <typename... Components>
struct PlanTraits<NormalizationPlan<Components...>> {
static constexpr bool valid = (ComponentTraits<Components>::valid && ...);
};
template <typename Values, typename Residuals> struct MakeIdentityPlan;
template <typename... Values, typename... Residuals>
struct MakeIdentityPlan<
utils::blocks::type_list<Values...>,
utils::blocks::type_list<Residuals...>> {
using Type = NormalizationPlan<
CoordinateComponent<CoordinateKind::value, utils::blocks::type_list<Values>, IdentityCoordinate>...,
CoordinateComponent<
CoordinateKind::residual,
utils::blocks::type_list<Residuals>,
IdentityCoordinate>...>;
};
template <
NormalizationPrescription Prescription,
typename Values,
typename Residuals>
struct MakeRuntimePreparedPlan;
template <
NormalizationPrescription Prescription,
typename... Values,
typename... Residuals>
struct MakeRuntimePreparedPlan<
Prescription,
utils::blocks::type_list<Values...>,
utils::blocks::type_list<Residuals...>> {
using Method = RuntimePreparedCoordinate<Prescription>;
using Type = NormalizationPlan<
CoordinateComponent<
CoordinateKind::value,
utils::blocks::type_list<Values>,
Method>...,
CoordinateComponent<
CoordinateKind::residual,
utils::blocks::type_list<Residuals>,
Method>...>;
};
} // namespace detail
template <typename Candidate>
concept NormalizationPlanType = detail::PlanTraits<std::remove_cvref_t<Candidate>>::valid;
template <typename Form>
requires utils::blocks::block_form_is_valid_v<Form>
using IdentityNormalizationPlanFor = typename detail::MakeIdentityPlan<
typename Form::value_blocks,
typename Form::residual_blocks>::Type;
template <NormalizationPrescription Prescription, typename Form>
requires utils::blocks::block_form_is_valid_v<Form>
using RuntimePreparedNormalizationPlanFor =
typename detail::MakeRuntimePreparedPlan<
std::remove_cvref_t<Prescription>,
typename Form::value_blocks,
typename Form::residual_blocks>::Type;
template <typename Form, typename Plan>
requires utils::blocks::block_form_is_valid_v<Form>
struct NormalizationCoverage final {
using DeclaredValueBlocks = typename Plan::ValueBlocks;
using DeclaredResidualBlocks = typename Plan::ResidualBlocks;
using MissingValueBlocks = detail::ListDifferenceT<typename Form::value_blocks, DeclaredValueBlocks>;
using UnexpectedValueBlocks = detail::ListDifferenceT<DeclaredValueBlocks, typename Form::value_blocks>;
using RepeatedValueBlocks = detail::RepeatedTypesT<DeclaredValueBlocks>;
using MissingResidualBlocks = detail::ListDifferenceT<typename Form::residual_blocks, DeclaredResidualBlocks>;
using UnexpectedResidualBlocks = detail::ListDifferenceT<DeclaredResidualBlocks, typename Form::residual_blocks>;
using RepeatedResidualBlocks = detail::RepeatedTypesT<DeclaredResidualBlocks>;
static constexpr bool hasEveryValueBlock = MissingValueBlocks::size == 0;
static constexpr bool hasOnlyValueBlocks = UnexpectedValueBlocks::size == 0;
static constexpr bool hasUniqueValueOwners = RepeatedValueBlocks::size == 0;
static constexpr bool hasEveryResidualBlock = MissingResidualBlocks::size == 0;
static constexpr bool hasOnlyResidualBlocks = UnexpectedResidualBlocks::size == 0;
static constexpr bool hasUniqueResidualOwners = RepeatedResidualBlocks::size == 0;
static constexpr bool complete = hasEveryValueBlock && hasOnlyValueBlocks && hasUniqueValueOwners &&
hasEveryResidualBlock && hasOnlyResidualBlocks &&
hasUniqueResidualOwners;
};
template <typename Plan, typename Form>
concept CompleteNormalizationFor = utils::blocks::block_form_is_valid_v<Form> &&
NormalizationPlanType<Plan> &&
NormalizationCoverage<Form, std::remove_cvref_t<Plan>>::complete;
} // namespace mean_field::normalization

View File

@@ -0,0 +1,921 @@
module;
#include <concepts>
#include <cstdint>
#include <memory>
#include <span>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <utility>
#include <mfem.hpp>
export module mean_field:normalization.stellar_equilibrium;
export import :normalization.operators;
export import :operators.stellar_equilibrium_compiler;
export import :operators.stellar_equilibrium_problem;
export import :utils.domain;
namespace mean_field::normalization::detail {
using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema;
[[nodiscard]] inline mfem::Vector AssembleScalarMassDiagonal(
mfem::ParFiniteElementSpace &space,
mfem::Array<int> *domainMarker = nullptr
) {
mfem::ParBilinearForm mass(&space);
if (domainMarker == nullptr) {
mass.AddDomainIntegrator(new mfem::MassIntegrator());
} else {
mass.AddDomainIntegrator(new mfem::MassIntegrator(), *domainMarker);
}
mass.Assemble();
mass.Finalize();
std::unique_ptr<mfem::HypreParMatrix> matrix(mass.ParallelAssemble());
if (matrix == nullptr) {
throw std::runtime_error("Reference scalar Riesz mass assembly failed.");
}
mfem::Vector diagonal;
matrix->GetDiag(diagonal);
return diagonal;
}
[[nodiscard]] inline mfem::Vector AssembleHDivMassDiagonal(mfem::ParFiniteElementSpace &space) {
mfem::ParBilinearForm mass(&space);
mass.AddDomainIntegrator(new mfem::VectorFEMassIntegrator());
mass.Assemble();
mass.Finalize();
std::unique_ptr<mfem::HypreParMatrix> matrix(mass.ParallelAssemble());
if (matrix == nullptr) {
throw std::runtime_error("Reference H(div) Riesz mass assembly failed.");
}
mfem::Vector diagonal;
matrix->GetDiag(diagonal);
return diagonal;
}
[[nodiscard]] inline mfem::Vector AssembleSurfaceMassDiagonal(
const fem::FEM &finiteElements,
const field::ScalarBoundaryDofMap &surfaceMap
) {
mfem::Array<int> marker(finiteElements.mesh->bdr_attributes.Max());
marker = 0;
constexpr int attribute = DomainSchema::template boundary_attribute<utils::domain::StellarSurface>();
if (attribute <= 0 || attribute > marker.Size()) {
throw std::invalid_argument("The reference mesh does not contain the stellar-surface boundary.");
}
marker[attribute - 1] = 1;
mfem::ParBilinearForm mass(finiteElements.surfaceDeformationFes.get());
mass.AddBoundaryIntegrator(new mfem::MassIntegrator(), marker);
mass.Assemble();
mass.Finalize();
std::unique_ptr<mfem::HypreParMatrix> matrix(mass.ParallelAssemble());
if (matrix == nullptr) {
throw std::runtime_error("Reference surface Riesz mass assembly failed.");
}
mfem::Vector ambientDiagonal;
matrix->GetDiag(ambientDiagonal);
return surfaceMap.gather(ambientDiagonal);
}
template <equilibrium::DiscretizedStellarEquilibriumProblem Problem>
[[nodiscard]] const auto &PhysicalOperator(const Problem &problem) {
return problem.GetPhysicalOperator();
}
[[nodiscard]] inline mfem::Vector GatherDiagonal(
const mfem::Vector &fullDiagonal,
const field::FieldDofMap &map,
const char *role
) {
if (fullDiagonal.Size() != map.full_size()) {
throw std::logic_error(std::string("The reference ") + role + " Gram diagonal has an incompatible map.");
}
return map.gather(fullDiagonal);
}
} // namespace mean_field::normalization::detail
export namespace mean_field::normalization {
/*
* Runtime preparation paired with the compile-time normalization plan.
* The operator compiler is the authority for which blocks a specification
* generated, and PhysicalRieszBlockTraits is the authority for their
* declared physical laws. Keeping those responsibilities separate means
* this layer never names a concrete integral or phase constraint.
*/
namespace detail {
template <typename Block>
using PhysicalRieszMethodFor = typename PhysicalRieszBlockTraits<Block>::Method;
template <typename Block, typename = void>
struct IsGlobalGeneratedValueNormalization : std::false_type { };
template <typename Generated>
struct IsGlobalGeneratedValueNormalization<
utils::blocks::generated_value_block<Generated>,
std::void_t<
decltype(PhysicalRieszMethodFor<
utils::blocks::generated_value_block<Generated>>::topology),
decltype(PhysicalRieszMethodFor<
utils::blocks::generated_value_block<Generated>>::scale)>>
: std::bool_constant<
PhysicalRieszBlockTraits<
utils::blocks::generated_value_block<Generated>>::registered &&
PhysicalRieszMethodFor<
utils::blocks::generated_value_block<Generated>>::topology ==
RieszTopology::global_scalar> { };
template <typename Blocks, typename Specification>
struct GeneratedValueBlocksBelongToSpecification : std::false_type { };
template <typename Generated, typename Specification, typename = void>
struct GeneratedCoordinateBelongsToSpecification : std::false_type { };
template <typename Generated, typename Specification>
struct GeneratedCoordinateBelongsToSpecification<
Generated,
Specification,
std::void_t<typename Generated::SpecificationType>>
: std::bool_constant<
std::same_as<typename Generated::SpecificationType, Specification>> { };
template <typename Specification, typename... Generated>
struct GeneratedValueBlocksBelongToSpecification<
utils::blocks::type_list<utils::blocks::generated_value_block<Generated>...>,
Specification>
: std::bool_constant<
(GeneratedCoordinateBelongsToSpecification<Generated, Specification>::value && ...)> { };
template <typename Block, typename = void>
struct IsGlobalGeneratedResidualNormalization : std::false_type { };
template <typename Generated>
struct IsGlobalGeneratedResidualNormalization<
utils::blocks::generated_residual_block<Generated>,
std::void_t<
decltype(PhysicalRieszMethodFor<
utils::blocks::generated_residual_block<Generated>>::topology),
decltype(PhysicalRieszMethodFor<
utils::blocks::generated_residual_block<Generated>>::scale)>>
: std::bool_constant<
PhysicalRieszBlockTraits<
utils::blocks::generated_residual_block<Generated>>::registered &&
PhysicalRieszMethodFor<
utils::blocks::generated_residual_block<Generated>>::topology ==
RieszTopology::global_scalar> { };
template <typename Blocks, typename Specification>
struct GeneratedResidualBlocksBelongToSpecification : std::false_type { };
template <typename Specification, typename... Generated>
struct GeneratedResidualBlocksBelongToSpecification<
utils::blocks::type_list<utils::blocks::generated_residual_block<Generated>...>,
Specification>
: std::bool_constant<
(GeneratedCoordinateBelongsToSpecification<Generated, Specification>::value && ...)> { };
template <typename Blocks> struct PrepareGeneratedValueNormalizations {
static constexpr bool registered = false;
template <typename Form>
static constexpr bool completeFor = false;
template <typename Form>
static void Apply(
DiagonalNormalizationBuilder<Form> &,
const StellarCharacteristicScales &
) {
static_assert(registered, "Generated value-block normalization metadata is malformed.");
}
};
template <typename... Blocks>
struct PrepareGeneratedValueNormalizations<utils::blocks::type_list<Blocks...>> {
static constexpr bool registered =
(IsGlobalGeneratedValueNormalization<Blocks>::value && ...);
template <typename Form>
static constexpr bool completeFor = registered &&
utils::blocks::block_form_is_valid_v<Form> &&
(utils::blocks::contains_type_v<Blocks, typename Form::value_blocks> && ...);
template <typename Form>
static void Apply(
DiagonalNormalizationBuilder<Form> &builder,
const StellarCharacteristicScales &scales
) {
if constexpr (completeFor<Form>) {
(builder.template SetValueGlobal<Blocks>(physicalScale<Blocks>(scales)), ...);
} else {
static_assert(
completeFor<Form>,
"Every generated value block must have a declared global-scalar Physical Riesz law "
"and belong to the compiled equilibrium form."
);
}
}
};
template <typename Blocks> struct PrepareGeneratedResidualNormalizations {
static constexpr bool registered = false;
template <typename Form>
static constexpr bool completeFor = false;
template <typename Form>
static void Apply(
DiagonalNormalizationBuilder<Form> &,
const StellarCharacteristicScales &
) {
static_assert(registered, "Generated residual-block normalization metadata is malformed.");
}
};
template <typename... Blocks>
struct PrepareGeneratedResidualNormalizations<utils::blocks::type_list<Blocks...>> {
static constexpr bool registered =
(IsGlobalGeneratedResidualNormalization<Blocks>::value && ...);
template <typename Form>
static constexpr bool completeFor = registered &&
utils::blocks::block_form_is_valid_v<Form> &&
(utils::blocks::contains_type_v<Blocks, typename Form::residual_blocks> && ...);
template <typename Form>
static void Apply(
DiagonalNormalizationBuilder<Form> &builder,
const StellarCharacteristicScales &scales
) {
if constexpr (completeFor<Form>) {
(builder.template SetResidualGlobal<Blocks>(physicalScale<Blocks>(scales)), ...);
} else {
static_assert(
completeFor<Form>,
"Every generated residual block must have a declared global-scalar Physical Riesz law "
"and belong to the compiled equilibrium form."
);
}
}
};
template <typename Specification, typename = void>
struct CompileStellarSpecificationNormalization {
using ValuePreparation = PrepareGeneratedValueNormalizations<void>;
using ResidualPreparation = PrepareGeneratedResidualNormalizations<void>;
static constexpr bool registered = false;
template <typename Form>
static constexpr bool completeFor = false;
template <typename Form>
static void Apply(
DiagonalNormalizationBuilder<Form> &,
const StellarCharacteristicScales &
) {
static_assert(
completeFor<Form>,
"The specification has no complete generated-coordinate normalization."
);
}
};
template <models::ModelSpecification Specification>
struct CompileStellarSpecificationNormalization<
Specification,
std::void_t<
typename operators::StellarEquilibriumSpecificationCompilation<
Specification>::GeneratedValueBlocks,
typename operators::StellarEquilibriumSpecificationCompilation<
Specification>::GeneratedResidualBlocks>> {
using OperatorCompilation =
operators::StellarEquilibriumSpecificationCompilation<Specification>;
using ValuePreparation = PrepareGeneratedValueNormalizations<
typename OperatorCompilation::GeneratedValueBlocks>;
using ResidualPreparation = PrepareGeneratedResidualNormalizations<
typename OperatorCompilation::GeneratedResidualBlocks>;
static constexpr bool registered = OperatorCompilation::complete &&
models::CompleteGeneratedNormalizationFor<
Specification> &&
GeneratedValueBlocksBelongToSpecification<
typename OperatorCompilation::GeneratedValueBlocks,
Specification>::value &&
GeneratedResidualBlocksBelongToSpecification<
typename OperatorCompilation::GeneratedResidualBlocks,
Specification>::value &&
ValuePreparation::registered &&
ResidualPreparation::registered;
template <typename Form>
static constexpr bool completeFor = registered &&
ValuePreparation::template completeFor<Form> &&
ResidualPreparation::template completeFor<Form>;
template <typename Form>
static void Apply(
DiagonalNormalizationBuilder<Form> &builder,
const StellarCharacteristicScales &scales
) {
if constexpr (completeFor<Form>) {
ValuePreparation::template Apply<Form>(builder, scales);
ResidualPreparation::template Apply<Form>(builder, scales);
} else {
static_assert(
completeFor<Form>,
"The specification's generated blocks do not have a complete runtime normalization."
);
}
}
};
template <typename SpecificationSet> struct PrepareSpecificationNormalizations;
template <models::ModelSpecification... Specifications>
struct PrepareSpecificationNormalizations<models::detail::SpecificationSetStorage<Specifications...>> {
static constexpr bool registered =
(CompileStellarSpecificationNormalization<Specifications>::registered && ...);
template <typename Form>
static constexpr bool completeFor =
(CompileStellarSpecificationNormalization<Specifications>::template completeFor<Form> && ...);
template <typename Form>
static void Apply(
DiagonalNormalizationBuilder<Form> &builder,
const StellarCharacteristicScales &scales
) {
static_assert(
completeFor<Form>,
"Every generated stellar-equilibrium coordinate requires a declared global-scalar "
"Physical Riesz normalization and compiler-owned root block."
);
(CompileStellarSpecificationNormalization<Specifications>::template Apply<Form>(builder, scales), ...);
}
};
template <typename Model, typename Form, typename = void>
struct StellarModelNormalizationCoverage : std::false_type { };
template <typename Model, typename Form>
requires model::StellarModelType<Model> && utils::blocks::block_form_is_valid_v<Form>
struct StellarModelNormalizationCoverage<
Model,
Form,
std::void_t<typename std::remove_cvref_t<Model>::SpecificationTypes>>
: std::bool_constant<
PrepareSpecificationNormalizations<
typename std::remove_cvref_t<Model>::SpecificationTypes>::template completeFor<Form>> { };
} // namespace detail
template <typename Specification>
struct StellarSpecificationNormalizationContribution
: detail::CompileStellarSpecificationNormalization<std::remove_cvref_t<Specification>> {
using Base = detail::CompileStellarSpecificationNormalization<std::remove_cvref_t<Specification>>;
template <typename Form>
static void Apply(
DiagonalNormalizationBuilder<Form> &builder,
const StellarCharacteristicScales &scales
) {
static_assert(
Base::template completeFor<Form>,
"The specification's generated blocks do not have a complete runtime normalization."
);
Base::template Apply<Form>(builder, scales);
}
};
template <typename Specification>
concept RegisteredStellarSpecificationNormalization =
StellarSpecificationNormalizationContribution<Specification>::registered;
template <typename Specification, typename Form>
concept CompleteStellarSpecificationNormalizationFor =
utils::blocks::block_form_is_valid_v<Form> &&
StellarSpecificationNormalizationContribution<Specification>::template completeFor<Form>;
template <typename Model, typename Form>
concept CompleteStellarNormalizationFor =
detail::StellarModelNormalizationCoverage<
std::remove_cvref_t<Model>,
std::remove_cvref_t<Form>>::value;
/*
* Physical Riesz preparation is an optional capability of a physical
* core, not part of the protocol needed by the variadic equilibrium root.
* Keeping this boundary structural lets a new EOS core opt in by exposing
* the same discretization maps without inheriting from, or otherwise
* naming, the Polytrope implementation.
*/
template <typename Candidate>
concept PhysicalRieszStellarEquilibriumCore =
operators::PreparedStellarEquilibriumPhysicalCore<std::remove_cvref_t<Candidate>> &&
PhysicalRieszCoreRuntime<std::remove_cvref_t<Candidate>>;
template <typename Problem>
concept PhysicalRieszStellarEquilibriumProblem =
equilibrium::DiscretizedStellarEquilibriumProblem<std::remove_cvref_t<Problem>> &&
requires {
typename std::remove_cvref_t<Problem>::ModelType;
typename std::remove_cvref_t<Problem>::FormType;
typename std::remove_cvref_t<Problem>::PhysicalCoreType;
typename std::remove_cvref_t<Problem>::NormalizationPrescriptionType;
requires PhysicalRieszDiagonalPrescription<
typename std::remove_cvref_t<Problem>::NormalizationPrescriptionType>;
requires CompilableNormalizationFor<
typename std::remove_cvref_t<Problem>::NormalizationPrescriptionType,
typename std::remove_cvref_t<Problem>::FormType>;
requires CompleteStellarNormalizationFor<
typename std::remove_cvref_t<Problem>::ModelType,
typename std::remove_cvref_t<Problem>::FormType>;
requires StellarNormalizationRuntimeAvailableFor<
typename std::remove_cvref_t<Problem>::NormalizationPrescriptionType,
typename std::remove_cvref_t<Problem>::FormType,
typename std::remove_cvref_t<Problem>::PhysicalCoreType,
typename std::remove_cvref_t<Problem>::ModelType::SpecificationTypes>;
};
template <equilibrium::DiscretizedStellarEquilibriumProblem Problem>
requires std::same_as<
typename std::remove_cvref_t<Problem>::NormalizationPrescriptionType,
Unnormalized>
[[nodiscard]] DiagonalNormalization prepareNormalization(const Problem &problem) {
return DiagonalNormalization::Identity(problem.StateSize(), problem.EquationSize());
}
template <PhysicalRieszStellarEquilibriumProblem Problem>
[[nodiscard]] DiagonalNormalization prepareNormalization(const Problem &problem) {
using ProblemType = std::remove_cvref_t<Problem>;
using Form = typename ProblemType::FormType;
const fem::FEM &finiteElements = problem.GetDiscretization().finiteElementModel();
if (!finiteElements.okay()) {
throw std::invalid_argument("Physical Riesz preparation requires a current finite-element model.");
}
const auto &physical = detail::PhysicalOperator(problem);
const auto &gravityContext = physical.GetGravityContext();
const auto &enthalpyMap = physical.GetHydrostaticOperator().GetEnthalpyMap();
const auto scales = deriveStellarCharacteristicScales(
problem.GetNormalizationPrescription(),
problem.GetStellarModel()
);
mfem::Array<int> stellarMarker =
utils::domain::make_attribute_marker<utils::domain::Stellar, detail::DomainSchema>(*finiteElements.mesh);
const mfem::Vector densityDiagonal = detail::GatherDiagonal(
detail::AssembleScalarMassDiagonal(*finiteElements.densityFes, &stellarMarker),
gravityContext.GetDensityMap(),
"density"
);
const mfem::Vector enthalpyDiagonal = detail::GatherDiagonal(
detail::AssembleScalarMassDiagonal(*finiteElements.enthalpyFes, &stellarMarker),
enthalpyMap,
"enthalpy"
);
const mfem::Vector gravityGradientDiagonal = detail::GatherDiagonal(
detail::AssembleHDivMassDiagonal(*finiteElements.gravityFluxFes),
gravityContext.GetGravityGradientMap(),
"gravity-gradient"
);
const mfem::Vector gravityPotentialDiagonal = detail::GatherDiagonal(
detail::AssembleScalarMassDiagonal(*finiteElements.gravityPotentialFes),
gravityContext.GetGravityPotentialMap(),
"gravity-potential"
);
const field::ScalarBoundaryDofMap surfaceMap =
field::make_stellar_surface_scalar_dof_map<detail::DomainSchema>(*finiteElements.surfaceDeformationFes);
const mfem::Vector surfaceDiagonal = detail::AssembleSurfaceMassDiagonal(finiteElements, surfaceMap);
if (surfaceDiagonal.Size() != physical.GetDomainDeformation().parameterCount()) {
throw std::logic_error("The reference surface Gram diagonal does not match the root surface block.");
}
DiagonalNormalizationBuilder<Form> builder(problem.GetManifest().layout());
builder.template SetValueBlock<utils::blocks::density::mass::value>(
physicalScale<utils::blocks::density::mass::value>(scales), densityDiagonal
);
builder.template SetValueBlock<utils::blocks::surface_deformation::parameters::value>(
physicalScale<utils::blocks::surface_deformation::parameters::value>(scales), surfaceDiagonal
);
builder.template SetValueBlock<utils::blocks::gravity::gradient::value>(
physicalScale<utils::blocks::gravity::gradient::value>(scales), gravityGradientDiagonal
);
builder.template SetValueBlock<utils::blocks::gravity::poisson::value>(
physicalScale<utils::blocks::gravity::poisson::value>(scales), gravityPotentialDiagonal
);
builder.template SetValueBlock<utils::blocks::enthalpy::specific::value>(
physicalScale<utils::blocks::enthalpy::specific::value>(scales), enthalpyDiagonal
);
builder.template SetResidualBlock<utils::blocks::gravity::gradient::residual>(
physicalScale<utils::blocks::gravity::gradient::residual>(scales), gravityGradientDiagonal
);
builder.template SetResidualBlock<utils::blocks::gravity::poisson::residual>(
physicalScale<utils::blocks::gravity::poisson::residual>(scales), gravityPotentialDiagonal
);
builder.template SetResidualBlock<utils::blocks::density::mass::residual>(
physicalScale<utils::blocks::density::mass::residual>(scales), densityDiagonal
);
builder.template SetResidualBlock<utils::blocks::surface_deformation::shape_equilibrium::residual>(
physicalScale<utils::blocks::surface_deformation::shape_equilibrium::residual>(scales), surfaceDiagonal
);
const mfem::Array<int> &surfaceRows = problem.GetPressureSurfaceRows().reduced_dofs();
builder.template SetHybridResidualBlock<utils::blocks::enthalpy::specific::residual>(
physicalScale<utils::blocks::enthalpy::specific::residual>(scales),
enthalpyDiagonal,
std::span<const int>{surfaceRows.GetData(), static_cast<std::size_t>(surfaceRows.Size())}
);
detail::PrepareSpecificationNormalizations<typename ProblemType::ModelType::SpecificationTypes>::Apply(
builder,
scales
);
return std::move(builder).Build();
}
/* Public adapter for a third-party prescription. The implementation stays
* beside the policy and has the readable signature
*
* prepareStellarNormalization(policy, problem)
*
* while every solver-facing caller continues to use the uniform
* prepareNormalization(problem) operation. */
template <equilibrium::DiscretizedStellarEquilibriumProblem Problem>
requires(
!std::same_as<
typename std::remove_cvref_t<Problem>::NormalizationPrescriptionType,
Unnormalized> &&
!PhysicalRieszDiagonalPrescription<
typename std::remove_cvref_t<Problem>::NormalizationPrescriptionType> &&
RuntimePreparedNormalizationOperation<Problem>)
[[nodiscard]] DiagonalNormalization prepareNormalization(
const Problem &problem
) {
return prepareStellarNormalization(
problem.GetNormalizationPrescription(),
problem
);
}
/*
* Solver-facing normalization exists exactly when runtime preparation for
* the problem's compile-time prescription is a valid operation. This
* folds future policy-owned preparation hooks into the same public contract and
* turns unsupported core/prescription pairs into ordinary constraint
* failure instead of an error in a constructor body.
*/
template <typename Problem>
concept NormalizableStellarEquilibriumProblem =
equilibrium::DiscretizedStellarEquilibriumProblem<std::remove_cvref_t<Problem>> &&
requires(const std::remove_cvref_t<Problem> &problem) {
{
prepareNormalization(problem)
} -> std::same_as<DiagonalNormalization>;
};
struct NormalizedStellarEquilibriumStatistics final {
std::uint64_t normalizationPreparations{0};
std::uint64_t physicalPreparations{0};
std::uint64_t residualRetrievals{0};
std::uint64_t jacobianApplications{0};
};
/*
* The high-level stellar adapter retains a pointer to a prepared inverse.
* Consequently that inverse must identify the exact physical problem and
* expose its lifecycle state. Generic MFEM solvers remain valid inputs to
* the lower-level ScaledPreconditioner, where no stellar association is
* implied.
*/
template <typename Candidate, typename Problem>
concept ProblemBoundStellarInverseFor =
NormalizableStellarEquilibriumProblem<std::remove_cvref_t<Problem>> &&
std::derived_from<std::remove_cvref_t<Candidate>, mfem::Solver> &&
requires(const std::remove_cvref_t<Candidate> &inverse) {
{
inverse.GetProblem()
} -> std::same_as<const std::remove_cvref_t<Problem> &>;
{
inverse.IsCurrent()
} -> std::same_as<bool>;
};
template <NormalizableStellarEquilibriumProblem Problem, typename PhysicalInverse>
requires ProblemBoundStellarInverseFor<PhysicalInverse, Problem>
class NormalizedStellarPreconditioner;
/*
* Solver-facing coordinates for a dimensional stellar problem. The
* physical problem remains the sole source of residual and Jacobian
* physics; this adapter performs only the coordinate maps
*
* x = R x_hat, F_hat = L F, J_hat = L J R.
*
* Its normalization is immutable during Prepare/BuildResidual/Mult and is
* changed only by an explicit RefreshNormalization call.
*/
template <NormalizableStellarEquilibriumProblem Problem>
class NormalizedStellarEquilibriumOperator final : public mfem::Operator {
private:
using ProblemType = std::remove_cvref_t<Problem>;
public:
explicit NormalizedStellarEquilibriumOperator(ProblemType &problem)
: mfem::Operator(problem.EquationSize(), problem.StateSize()),
m_problem(&problem),
m_normalization(prepareNormalization(problem)),
m_scaledJacobian(problem.GetLinearizationOperator(), m_normalization),
m_physicalState(problem.StateSize()),
m_physicalResidual(problem.EquationSize()),
m_normalizedResidual(problem.EquationSize()) {
if (Width() != Height()) {
throw std::invalid_argument("A normalized stellar-equilibrium operator must be square.");
}
m_statistics.normalizationPreparations = 1;
}
NormalizedStellarEquilibriumOperator(const NormalizedStellarEquilibriumOperator &) = delete;
NormalizedStellarEquilibriumOperator &operator=(const NormalizedStellarEquilibriumOperator &) = delete;
NormalizedStellarEquilibriumOperator(NormalizedStellarEquilibriumOperator &&) = delete;
NormalizedStellarEquilibriumOperator &operator=(NormalizedStellarEquilibriumOperator &&) = delete;
[[nodiscard]] auto Prepare(
const mfem::Vector &normalizedState,
const operators::StellarEquilibriumDependencies &dependencies,
const physics::RigidRotation &rotation
) requires(ProblemType::generatedRotationProviderCount == 0) {
if (normalizedState.Size() != Width()) {
throw std::invalid_argument("The normalized stellar state has the wrong size.");
}
m_isPrepared = false;
m_normalization.DenormalizeState(normalizedState, m_physicalState);
auto report = m_problem->Prepare(m_physicalState, dependencies, rotation);
m_problem->BuildResidual(m_physicalResidual);
m_normalization.NormalizeResidual(m_physicalResidual, m_normalizedResidual);
m_physicalPreparationGeneration = m_problem->GetPreparationGeneration();
m_isPrepared = true;
++m_statistics.physicalPreparations;
return report;
}
[[nodiscard]] auto Prepare(
const mfem::Vector &normalizedState,
const operators::StellarEquilibriumDependencies &dependencies
) requires(ProblemType::generatedRotationProviderCount == 1) {
if (normalizedState.Size() != Width()) {
throw std::invalid_argument("The normalized stellar state has the wrong size.");
}
m_isPrepared = false;
m_normalization.DenormalizeState(normalizedState, m_physicalState);
auto report = m_problem->Prepare(m_physicalState, dependencies);
m_problem->BuildResidual(m_physicalResidual);
m_normalization.NormalizeResidual(m_physicalResidual, m_normalizedResidual);
m_physicalPreparationGeneration = m_problem->GetPreparationGeneration();
m_isPrepared = true;
++m_statistics.physicalPreparations;
return report;
}
void BuildResidual(mfem::Vector &normalizedResidual) const {
VerifyPrepared();
normalizedResidual = m_normalizedResidual;
++m_statistics.residualRetrievals;
}
void Mult(
const mfem::Vector &normalizedDirection,
mfem::Vector &normalizedAction
) const override {
VerifyPrepared();
if (normalizedDirection.Size() != Width()) {
throw std::invalid_argument("The normalized stellar direction has the wrong size.");
}
m_scaledJacobian.Mult(normalizedDirection, normalizedAction);
++m_statistics.jacobianApplications;
}
void RefreshNormalization() {
DiagonalNormalization refreshed = prepareNormalization(*m_problem);
m_normalization = std::move(refreshed);
m_isPrepared = false;
++m_statistics.normalizationPreparations;
}
void NormalizeState(
const mfem::Vector &physicalState,
mfem::Vector &normalizedState
) const {
m_normalization.NormalizeState(physicalState, normalizedState);
}
void DenormalizeState(
const mfem::Vector &normalizedState,
mfem::Vector &physicalState
) const {
m_normalization.DenormalizeState(normalizedState, physicalState);
}
void NormalizeResidual(
const mfem::Vector &physicalResidual,
mfem::Vector &normalizedResidual
) const {
m_normalization.NormalizeResidual(physicalResidual, normalizedResidual);
}
void DenormalizeResidual(
const mfem::Vector &normalizedResidual,
mfem::Vector &physicalResidual
) const {
m_normalization.DenormalizeResidual(normalizedResidual, physicalResidual);
}
template <typename PhysicalInverse>
requires ProblemBoundStellarInverseFor<PhysicalInverse, Problem>
[[nodiscard]] NormalizedStellarPreconditioner<Problem, std::remove_cvref_t<PhysicalInverse>>
MakeScaledPreconditioner(PhysicalInverse &physicalInverse) const;
[[nodiscard]] bool IsPrepared() const noexcept {
return m_isPrepared && m_problem->IsPrepared() &&
m_physicalPreparationGeneration == m_problem->GetPreparationGeneration();
}
[[nodiscard]] ProblemType &GetPhysicalProblem() noexcept {
return *m_problem;
}
[[nodiscard]] const ProblemType &GetPhysicalProblem() const noexcept {
return *m_problem;
}
[[nodiscard]] const mfem::Operator &GetPhysicalJacobian() const noexcept {
return m_problem->GetLinearizationOperator();
}
[[nodiscard]] const ProblemType &GetProblem() const noexcept {
return *m_problem;
}
[[nodiscard]] const DiagonalNormalization &GetNormalization() const noexcept {
return m_normalization;
}
[[nodiscard]] const mfem::Vector &GetPhysicalState() const {
VerifyPrepared();
return m_physicalState;
}
[[nodiscard]] const mfem::Vector &GetPhysicalResidual() const {
VerifyPrepared();
return m_physicalResidual;
}
[[nodiscard]] const NormalizedStellarEquilibriumStatistics &GetStatistics() const noexcept {
return m_statistics;
}
private:
void VerifyPrepared() const {
if (!IsPrepared()) {
throw std::logic_error(
"The normalized stellar-equilibrium operator must be prepared and current before application."
);
}
}
ProblemType *m_problem;
DiagonalNormalization m_normalization;
ScaledJacobianOperator m_scaledJacobian;
mfem::Vector m_physicalState;
mfem::Vector m_physicalResidual;
mfem::Vector m_normalizedResidual;
std::uint64_t m_physicalPreparationGeneration{0};
mutable NormalizedStellarEquilibriumStatistics m_statistics;
bool m_isPrepared{false};
};
template <NormalizableStellarEquilibriumProblem Problem, typename PhysicalInverse>
requires ProblemBoundStellarInverseFor<PhysicalInverse, Problem>
class NormalizedStellarPreconditioner final : public mfem::Solver {
private:
using ProblemType = std::remove_cvref_t<Problem>;
using NormalizedOperator = NormalizedStellarEquilibriumOperator<ProblemType>;
using PhysicalInverseType = std::remove_cvref_t<PhysicalInverse>;
[[nodiscard]] static PhysicalInverseType &RequireAssociatedPhysicalInverse(
const NormalizedOperator &normalizedOperator,
PhysicalInverseType &physicalInverse
) {
if (std::addressof(physicalInverse.GetProblem()) !=
std::addressof(normalizedOperator.GetProblem())) {
throw std::invalid_argument(
"A normalized stellar preconditioner and its physical inverse must belong to the same problem."
);
}
return physicalInverse;
}
public:
NormalizedStellarPreconditioner(
const NormalizedOperator &normalizedOperator,
PhysicalInverseType &physicalInverse
)
: mfem::Solver(
normalizedOperator.Width(),
normalizedOperator.Height(),
physicalInverse.iterative_mode
),
m_normalizedOperator(&normalizedOperator),
m_physicalInverse(&physicalInverse),
m_scaled(
RequireAssociatedPhysicalInverse(normalizedOperator, physicalInverse),
normalizedOperator.GetPhysicalJacobian(),
normalizedOperator,
normalizedOperator.GetNormalization()
) {
}
NormalizedStellarPreconditioner(const NormalizedStellarPreconditioner &) = delete;
NormalizedStellarPreconditioner &operator=(const NormalizedStellarPreconditioner &) = delete;
NormalizedStellarPreconditioner(NormalizedStellarPreconditioner &&) = delete;
NormalizedStellarPreconditioner &operator=(NormalizedStellarPreconditioner &&) = delete;
void SetOperator(const mfem::Operator &normalizedJacobian) override {
VerifyCurrent();
if (&normalizedJacobian != m_normalizedOperator) {
throw std::invalid_argument(
"The normalized stellar preconditioner cannot be rebound to a different Jacobian."
);
}
m_scaled.SetOperator(normalizedJacobian);
}
void Mult(
const mfem::Vector &normalizedResidual,
mfem::Vector &normalizedCorrection
) const override {
VerifyCurrent();
m_scaled.Mult(normalizedResidual, normalizedCorrection);
}
[[nodiscard]] bool IsCurrent() const {
return m_normalizedOperator->IsPrepared() &&
m_physicalInverse->IsCurrent();
}
[[nodiscard]] PhysicalInverseType &GetPhysicalInverse() noexcept {
return *m_physicalInverse;
}
[[nodiscard]] const PhysicalInverseType &GetPhysicalInverse() const noexcept {
return *m_physicalInverse;
}
[[nodiscard]] const mfem::Operator &GetPhysicalJacobian() const noexcept {
return m_scaled.GetPhysicalJacobian();
}
[[nodiscard]] const mfem::Operator &GetNormalizedJacobian() const {
return m_scaled.GetNormalizedJacobian();
}
[[nodiscard]] const ScaledPreconditionerStatistics &GetStatistics() const noexcept {
return m_scaled.GetStatistics();
}
private:
void VerifyCurrent() const {
if (!IsCurrent()) {
throw std::logic_error(
"The normalized stellar preconditioner cannot be used while its normalized operator or physical "
"inverse is stale."
);
}
}
const NormalizedOperator *m_normalizedOperator;
PhysicalInverseType *m_physicalInverse;
ScaledPreconditioner m_scaled;
};
template <NormalizableStellarEquilibriumProblem Problem>
template <typename PhysicalInverse>
requires ProblemBoundStellarInverseFor<PhysicalInverse, Problem>
NormalizedStellarPreconditioner<Problem, std::remove_cvref_t<PhysicalInverse>>
NormalizedStellarEquilibriumOperator<Problem>::MakeScaledPreconditioner(PhysicalInverse &physicalInverse) const {
VerifyPrepared();
return NormalizedStellarPreconditioner<Problem, std::remove_cvref_t<PhysicalInverse>>{
*this,
physicalInverse
};
}
template <NormalizableStellarEquilibriumProblem Problem>
[[nodiscard]] auto makeNormalizedStellarEquilibriumOperator(Problem &problem) {
return NormalizedStellarEquilibriumOperator<Problem>{problem};
}
} // namespace mean_field::normalization

View File

@@ -0,0 +1,196 @@
module;
#include <compare>
#include <cstdint>
#include <vector>
#include <mfem.hpp>
export module mean_field:operators.prepared_angular_momentum;
export import :fem;
export import :mapping.domain_mapper;
export import :model.compiled_fixed_angular_momentum;
export import :operators.context.gravity_field;
export namespace mean_field::operators {
struct AngularMomentumDependencyStamp final {
std::uint64_t identity{0};
std::uint64_t revision{0};
constexpr auto operator<=>(const AngularMomentumDependencyStamp &) const = default;
};
struct AngularMomentumDependencies final {
AngularMomentumDependencyStamp discretization;
AngularMomentumDependencyStamp density;
AngularMomentumDependencyStamp displacement;
AngularMomentumDependencyStamp rotation;
constexpr auto operator<=>(const AngularMomentumDependencies &) const = default;
};
struct PreparedAngularMomentumReport final {
bool rebuiltStaticPlan{false};
bool refreshedGeometry{false};
bool refreshedDensity{false};
bool updatedAngularVelocity{false};
bool assembledResidual{false};
[[nodiscard]] bool DidAnyWork() const noexcept {
return rebuiltStaticPlan || refreshedGeometry || refreshedDensity || updatedAngularVelocity ||
assembledResidual;
}
constexpr auto operator<=>(const PreparedAngularMomentumReport &) const = default;
};
struct AngularMomentumConstraintReport final {
double targetAngularMomentum;
double achievedAngularMomentum;
double momentOfInertia;
double angularVelocity;
double dimensionalResidual;
double scaledResidual;
};
struct PreparedAngularMomentumActionStatistics final {
std::uint64_t densityApplications{0};
std::uint64_t displacementApplications{0};
std::uint64_t angularVelocityApplications{0};
std::uint64_t completeApplications{0};
constexpr auto operator<=>(const PreparedAngularMomentumActionStatistics &) const = default;
};
/*
* Prepared scalar invariant
*
* R_J(rho, d, Omega) = Omega I_axis(rho, d) - J_target,
* I_axis = integral rho |(x-x_0)_perp|^2 dV.
*
* The axis is normalized by CompiledFixedAngularMomentum. Density and
* geometry are borrowed from the shared gravity context, so this row is
* linearized at exactly the same mapped state as every physical equation.
*/
class PreparedAngularMomentumOperator final {
public:
using SpecificationType = models::FixedAngularMomentum;
using CompiledConstraintType = models::CompiledFixedAngularMomentum;
using Dependencies = AngularMomentumDependencies;
using Report = PreparedAngularMomentumReport;
PreparedAngularMomentumOperator(
const fem::FEM &f,
const mapping::DomainMapper &domainMapper,
const context::gravity_field::GravityFieldLinearizationContext &gravityContext,
models::CompiledFixedAngularMomentum constraint
);
PreparedAngularMomentumOperator(const PreparedAngularMomentumOperator &) = delete;
PreparedAngularMomentumOperator &operator=(const PreparedAngularMomentumOperator &) = delete;
PreparedAngularMomentumOperator(PreparedAngularMomentumOperator &&) = delete;
PreparedAngularMomentumOperator &operator=(PreparedAngularMomentumOperator &&) = delete;
PreparedAngularMomentumReport Prepare(
double angularVelocity,
const AngularMomentumDependencies &dependencies
);
void BuildResidual(mfem::Vector &residual) const;
void ApplyDensityJacobianAction(
const mfem::Vector &densityVariation,
mfem::Vector &action
) const;
void ApplyDisplacementJacobianAction(
const mfem::Vector &displacementVariation,
mfem::Vector &action
) const;
void ApplyAngularVelocityJacobianAction(
double angularVelocityVariation,
mfem::Vector &action
) const;
void ApplyCompleteJacobianAction(
const mfem::Vector &densityVariation,
const mfem::Vector &displacementVariation,
double angularVelocityVariation,
mfem::Vector &action
) const;
[[nodiscard]] bool IsPrepared() const noexcept;
[[nodiscard]] double GetMomentOfInertia() const;
[[nodiscard]] double GetAngularVelocity() const;
[[nodiscard]] double GetCurrentAngularMomentum() const;
[[nodiscard]] double GetTargetAngularMomentum() const noexcept;
[[nodiscard]] physics::RigidRotation GetRotation() const;
[[nodiscard]] AngularMomentumConstraintReport GetConstraintReport() const;
[[nodiscard]] std::uint64_t GetPreparationCount() const noexcept;
[[nodiscard]] std::uint64_t GetResidualApplicationCount() const noexcept;
[[nodiscard]] const PreparedAngularMomentumActionStatistics &GetActionStatistics() const noexcept;
[[nodiscard]] const models::CompiledFixedAngularMomentum &GetCompiledConstraint() const noexcept;
private:
struct QuadraturePointData final {
mfem::IntegrationPoint integrationPoint;
mfem::Vector densityShape;
mapping::VolumeMappingContext mappingContext;
double density{0.0};
double cylindricalRadiusSquared{0.0};
};
struct ElementPAData final {
int elementId{-1};
mfem::Array<int> densityDofs;
mfem::Array<int> displacementDofs;
mfem::Array<int> compactificationDofs;
mfem::DofTransformation *densityDofTransformation{nullptr};
mfem::DofTransformation *displacementDofTransformation{nullptr};
mfem::DofTransformation *compactificationDofTransformation{nullptr};
mfem::Vector baseDisplacement;
mfem::Vector compactification;
std::vector<QuadraturePointData> quadraturePoints;
};
void BuildStaticPlan();
void RefreshGeometry(const mfem::Vector &displacement);
void RefreshDensity(const mfem::Vector &density);
void AssembleResidual();
void VerifyPrepared() const;
[[nodiscard]] double EvaluateDensityMomentActionLocal(const mfem::Vector &densityVariation) const;
[[nodiscard]] double EvaluateDisplacementMomentActionLocal(const mfem::Vector &displacementVariation) const;
[[nodiscard]] double CylindricalRadiusSquared(const mfem::Vector &physicalPosition) const noexcept;
[[nodiscard]] double CylindricalRadiusSquaredVariation(
const mfem::Vector &physicalPosition,
const mfem::Vector &physicalPositionVariation
) const noexcept;
[[nodiscard]] double GlobalSum(double localValue) const;
const fem::FEM &m_fem;
const mapping::DomainMapper &m_domainMapper;
const context::gravity_field::GravityFieldLinearizationContext &m_gravityContext;
models::CompiledFixedAngularMomentum m_constraint;
std::vector<ElementPAData> m_elements;
AngularMomentumDependencies m_preparedDependencies;
mfem::Vector m_cachedResidual;
mutable mfem::Vector m_densityVariationTrue;
mutable mfem::Vector m_displacementVariationTrue;
mutable mfem::Vector m_densityVariationLocal;
mutable mfem::Vector m_displacementVariationLocal;
mutable mfem::Vector m_elementDensityVariation;
mutable mfem::Vector m_elementDisplacementVariation;
double m_momentOfInertia{0.0};
double m_angularVelocity{0.0};
double m_currentAngularMomentum{0.0};
std::uint64_t m_preparationCount{0};
mutable std::uint64_t m_residualApplicationCount{0};
mutable PreparedAngularMomentumActionStatistics m_actionStatistics;
bool m_isPrepared{false};
};
} // namespace mean_field::operators

View File

@@ -1,117 +0,0 @@
module;
#include <concepts>
#include <memory>
#include <type_traits>
#include <utility>
#include <mfem.hpp>
export module mean_field:operators.prepared_central_density_stellar_equilibrium;
export import :model.compiled_fixed_central_density;
export import :operators.prepared_central_density;
export import :operators.prepared_stellar_equilibrium;
export namespace mean_field::operators {
using CentralDensityStellarEquilibriumSpecificationModel = model::StellarModel<
models::
SpecificationSet<eos::Polytrope, models::FixedTotalMass, surface::Isobaric, models::FixedCentralDensity>>;
using CentralDensityStellarEquilibriumForm = utils::blocks::central_density_bordered_stellar_equilibrium_form;
using CentralDensityStellarEquilibriumJacobianForm =
utils::blocks::central_density_bordered_stellar_equilibrium_jacobian_form;
using CentralDensityStellarEquilibriumLayout = utils::blocks::form_layout<CentralDensityStellarEquilibriumForm>;
using CentralDensityStellarEquilibriumSystemManifest = EquilibriumSystemManifest<
CentralDensityStellarEquilibriumSpecificationModel,
CentralDensityStellarEquilibriumForm,
CentralDensityStellarEquilibriumJacobianForm>;
using CentralDensityStellarEquilibriumRootManifest = CentralDensityStellarEquilibriumSystemManifest;
struct PreparedCentralDensityStellarEquilibriumReport final {
PreparedStellarEquilibriumReport physical;
PreparedCentralDensityReport phase;
bool assembledResidual{false};
[[nodiscard]] bool DidAnyWork() const noexcept {
return physical.DidAnyWork() || phase.DidAnyWork() || assembledResidual;
}
};
class PreparedCentralDensityStellarEquilibriumOperator final : public mfem::Operator {
public:
PreparedCentralDensityStellarEquilibriumOperator(
fem::FEM &f,
const mapping::DomainMapper &domainMapper,
const eos::Polytrope &equationOfState,
models::CompiledFixedMass fixedMassConstraint,
PressureSurfaceConstraintView surfaceConstraint,
deformation::PreparedDomainDeformationRuntime domainDeformation,
models::CompiledFixedCentralDensity centralDensity
)
: PreparedCentralDensityStellarEquilibriumOperator(
f,
std::make_unique<PreparedStellarEquilibriumOperator>(
f,
domainMapper,
equationOfState,
std::move(fixedMassConstraint),
surfaceConstraint,
std::move(domainDeformation)
),
std::move(centralDensity),
MakeCenterDofMap(f)
) {
}
PreparedCentralDensityStellarEquilibriumOperator(const PreparedCentralDensityStellarEquilibriumOperator &) =
delete;
PreparedCentralDensityStellarEquilibriumOperator &
operator=(const PreparedCentralDensityStellarEquilibriumOperator &) = delete;
PreparedCentralDensityStellarEquilibriumOperator(PreparedCentralDensityStellarEquilibriumOperator &&) = delete;
PreparedCentralDensityStellarEquilibriumOperator &
operator=(PreparedCentralDensityStellarEquilibriumOperator &&) = delete;
PreparedCentralDensityStellarEquilibriumReport Prepare(
const mfem::Vector &state,
const StellarEquilibriumDependencies &dependencies,
const physics::RigidRotation &rotation
);
void BuildResidual(mfem::Vector &residual) const;
void Mult(
const mfem::Vector &direction,
mfem::Vector &action
) const override;
[[nodiscard]] bool IsPrepared() const noexcept;
[[nodiscard]] const CentralDensityStellarEquilibriumLayout &GetLayout() const noexcept;
[[nodiscard]] const CentralDensityStellarEquilibriumRootManifest &GetRootManifest() const noexcept;
[[nodiscard]] const PreparedStellarEquilibriumOperator &GetPhysicalOperator() const noexcept;
[[nodiscard]] const PreparedCentralDensityConstraint &GetCentralDensityConstraint() const noexcept;
[[nodiscard]] RootConstraintReport GetFixedMassReport() const;
[[nodiscard]] CentralDensityConstraintReport GetCentralDensityReport() const;
private:
static field::FieldPointDofMap MakeCenterDofMap(const fem::FEM &f);
PreparedCentralDensityStellarEquilibriumOperator(
fem::FEM &f,
std::unique_ptr<PreparedStellarEquilibriumOperator> physicalOperator,
models::CompiledFixedCentralDensity centralDensity,
field::FieldPointDofMap centerDof
);
void AssembleResidual();
void VerifyPrepared() const;
std::unique_ptr<PreparedStellarEquilibriumOperator> m_physicalOperator;
models::CompiledFixedCentralDensity m_centralDensity;
PreparedCentralDensityConstraint m_phaseConstraint;
CentralDensityStellarEquilibriumRootManifest m_rootManifest;
mfem::Vector m_cachedResidual;
bool m_isPrepared{false};
};
} // namespace mean_field::operators

View File

@@ -35,6 +35,7 @@ export namespace mean_field::operators {
std::uint64_t enthalpyApplications{0}; std::uint64_t enthalpyApplications{0};
std::uint64_t gravityPotentialApplications{0}; std::uint64_t gravityPotentialApplications{0};
std::uint64_t bernoulliConstantApplications{0}; std::uint64_t bernoulliConstantApplications{0};
std::uint64_t rotationAmplitudeApplications{0};
std::uint64_t combinedApplications{0}; std::uint64_t combinedApplications{0};
constexpr auto operator<=>(const PreparedHydrostaticAlgebraicJacobianStatistics &) const = default; constexpr auto operator<=>(const PreparedHydrostaticAlgebraicJacobianStatistics &) const = default;
@@ -118,6 +119,14 @@ export namespace mean_field::operators {
mfem::Vector &action mfem::Vector &action
) const; ) const;
// Differentiates a multiplicative change Omega -> (1 + alpha) Omega
// at the frozen rigid rotation. Since Psi_rotation is quadratic in
// Omega, this contributes -2 alpha Psi_rotation to the hydrostatic row.
void ApplyRotationAmplitudeJacobianAction(
double fractionalAngularVelocityVariation,
mfem::Vector &action
) const;
void ApplyAlgebraicJacobianAction( void ApplyAlgebraicJacobianAction(
const mfem::Vector &enthalpyVariation, const mfem::Vector &enthalpyVariation,
const mfem::Vector &gravityPotentialVariation, const mfem::Vector &gravityPotentialVariation,

View File

@@ -96,6 +96,20 @@ export namespace mean_field::operators {
class PreparedStellarEquilibriumOperator final : public mfem::Operator { class PreparedStellarEquilibriumOperator final : public mfem::Operator {
public: public:
/*
* Privileged aggregate runtimes can inspect this complete numerical
* core. Keep their allow-list on the concrete core itself: a custom
* EOS may reuse this class, but it cannot extend the class's backend
* privileges. Ordinary specifications use restricted nested physics
* and never interact with this list.
*/
using BackendSpecifications = models::ModelTypeList<
eos::Polytrope,
surface::Isobaric,
models::FixedTotalMass,
models::FixedAngularMomentum,
models::FixedCentralDensity>;
template <models::StellarModelType Model> template <models::StellarModelType Model>
requires std::same_as< requires std::same_as<
typename std::remove_cvref_t<Model>::EquationOfStateType, typename std::remove_cvref_t<Model>::EquationOfStateType,
@@ -174,6 +188,12 @@ export namespace mean_field::operators {
[[nodiscard]] const PreparedHydrostaticEquilibriumOperator &GetHydrostaticOperator() const noexcept; [[nodiscard]] const PreparedHydrostaticEquilibriumOperator &GetHydrostaticOperator() const noexcept;
[[nodiscard]] const PreparedDisplacementResidualOperator &GetDisplacementOperator() const noexcept; [[nodiscard]] const PreparedDisplacementResidualOperator &GetDisplacementOperator() const noexcept;
[[nodiscard]] const PreparedMassNormalizationOperator &GetMassNormalizationOperator() const noexcept; [[nodiscard]] const PreparedMassNormalizationOperator &GetMassNormalizationOperator() const noexcept;
[[nodiscard]] double ApplyDensityVolumeIntegralDensityAction(
const mfem::Vector &densityDirection
) const;
[[nodiscard]] double ApplyDensityVolumeIntegralSurfaceShapeAction(
const mfem::Vector &surfaceShapeDirection
) const;
[[nodiscard]] const PreparedPressureSurfaceConstraint &GetSurfaceConstraintOperator() const noexcept; [[nodiscard]] const PreparedPressureSurfaceConstraint &GetSurfaceConstraintOperator() const noexcept;
[[nodiscard]] const deformation::PreparedDomainDeformationRuntime &GetDomainDeformation() const noexcept; [[nodiscard]] const deformation::PreparedDomainDeformationRuntime &GetDomainDeformation() const noexcept;
[[nodiscard]] const mfem::Vector &GetSurfaceDeformationParameters() const; [[nodiscard]] const mfem::Vector &GetSurfaceDeformationParameters() const;
@@ -234,5 +254,6 @@ export namespace mean_field::operators {
mutable mfem::Vector m_fullMechanicalAction; mutable mfem::Vector m_fullMechanicalAction;
mutable mfem::Vector m_surfaceShapeAction; mutable mfem::Vector m_surfaceShapeAction;
mutable mfem::Vector m_pullbackDerivativeAction; mutable mfem::Vector m_pullbackDerivativeAction;
mutable mfem::Vector m_densityVolumeIntegralAction;
}; };
} // namespace mean_field::operators } // namespace mean_field::operators

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,815 @@
module;
#include <concepts>
#include <type_traits>
export module mean_field:operators.stellar_equilibrium_compiler;
export import :model.compiled_fixed_angular_momentum;
export import :model.compiled_fixed_central_density;
export import :model.typed_stellar;
export import :utils.blocks;
export namespace mean_field::operators {
/*
* A coupling is the symbolic statement that one Jacobian block may be
* nonzero. Specifications contribute these statements independently of
* the final row and column layout.
*/
template <typename ResidualBlock, typename ValueBlock>
struct StellarEquilibriumJacobianCoupling final {
using Residual = ResidualBlock;
using Value = ValueBlock;
using ResidualBlockType = ResidualBlock;
using ValueBlockType = ValueBlock;
};
template <typename ResidualBlock, typename ValueBlock>
using EquilibriumJacobianCoupling =
StellarEquilibriumJacobianCoupling<ResidualBlock, ValueBlock>;
namespace detail {
template <typename... Lists> struct ConcatenateBlockLists;
template <> struct ConcatenateBlockLists<> {
using Type = utils::blocks::type_list<>;
};
template <typename... Types>
struct ConcatenateBlockLists<utils::blocks::type_list<Types...>> {
using Type = utils::blocks::type_list<Types...>;
};
template <typename... First, typename... Second, typename... Remaining>
struct ConcatenateBlockLists<utils::blocks::type_list<First...>,
utils::blocks::type_list<Second...>,
Remaining...> {
using Type = typename ConcatenateBlockLists<
utils::blocks::type_list<First..., Second...>, Remaining...>::Type;
};
template <typename... Lists>
using ConcatenateBlockListsT = typename ConcatenateBlockLists<Lists...>::Type;
template <typename List, typename Type> struct AppendUniqueBlockType;
template <typename... Types, typename Type>
struct AppendUniqueBlockType<utils::blocks::type_list<Types...>, Type> {
using TypeValue = std::conditional_t<
utils::blocks::contains_type_v<Type, utils::blocks::type_list<Types...>>,
utils::blocks::type_list<Types...>,
utils::blocks::type_list<Types..., Type>>;
};
template <typename Accumulated, typename Remaining> struct UniqueBlockListImpl;
template <typename Accumulated>
struct UniqueBlockListImpl<Accumulated, utils::blocks::type_list<>> {
using Type = Accumulated;
};
template <typename Accumulated, typename Head, typename... Tail>
struct UniqueBlockListImpl<Accumulated,
utils::blocks::type_list<Head, Tail...>> {
using Type = typename UniqueBlockListImpl<
typename AppendUniqueBlockType<Accumulated, Head>::TypeValue,
utils::blocks::type_list<Tail...>>::Type;
};
template <typename List>
using UniqueBlockListT =
typename UniqueBlockListImpl<utils::blocks::type_list<>, List>::Type;
template <typename... Lists>
using UniqueConcatenateBlockListsT =
UniqueBlockListT<ConcatenateBlockListsT<Lists...>>;
template <typename Candidate> struct IsValueBlockList : std::false_type {};
template <typename... Blocks>
struct IsValueBlockList<utils::blocks::type_list<Blocks...>>
: std::bool_constant<
(std::derived_from<Blocks, utils::blocks::value_block_base> && ...) &&
utils::blocks::types_are_unique_v<
utils::blocks::type_list<Blocks...>>> {};
template <typename Candidate> struct IsResidualBlockList : std::false_type {};
template <typename... Blocks>
struct IsResidualBlockList<utils::blocks::type_list<Blocks...>>
: std::bool_constant<
(std::derived_from<Blocks, utils::blocks::residual_block_base> &&
...) &&
utils::blocks::types_are_unique_v<
utils::blocks::type_list<Blocks...>>> {};
template <typename GeneratedValues> struct GeneratedValueBlocksFor;
template <typename... GeneratedValues>
struct GeneratedValueBlocksFor<models::ModelTypeList<GeneratedValues...>> {
using Type = utils::blocks::type_list<
utils::blocks::generated_value_block<GeneratedValues>...>;
};
template <typename GeneratedResiduals> struct GeneratedResidualBlocksFor;
template <typename... GeneratedResiduals>
struct GeneratedResidualBlocksFor<
models::ModelTypeList<GeneratedResiduals...>> {
using Type = utils::blocks::type_list<
utils::blocks::generated_residual_block<GeneratedResiduals>...>;
};
/*
* One translation boundary turns physics-facing stellar names into backend
* blocks. Existing backend block types pass through unchanged, which keeps
* the advanced extension API open without making built-in physics declarations
* depend on utils.blocks.
*/
template <typename DeclaredDependency>
struct UnmappedStellarDependency final {};
template <typename Blocks, typename DeclaredDependency>
struct SingleGeneratedBlock {
using Type = UnmappedStellarDependency<DeclaredDependency>;
static constexpr bool available = false;
};
template <typename Block, typename DeclaredDependency>
struct SingleGeneratedBlock<utils::blocks::type_list<Block>,
DeclaredDependency> {
using Type = Block;
static constexpr bool available = true;
};
template <models::ModelSpecification Specification, typename Dependency>
struct StellarDependencyBlock {
using Type = UnmappedStellarDependency<Dependency>;
static constexpr bool mapped = false;
};
template <models::ModelSpecification Specification, typename Block>
requires(std::derived_from<Block, utils::blocks::value_block_base> ||
std::derived_from<Block, utils::blocks::residual_block_base>)
struct StellarDependencyBlock<Specification, Block> {
using Type = Block;
static constexpr bool mapped = true;
};
template <models::ModelSpecification Specification>
struct StellarDependencyBlock<Specification, models::stellar::state::Density> {
using Type = utils::blocks::density::mass::value;
static constexpr bool mapped = true;
};
template <models::ModelSpecification Specification>
struct StellarDependencyBlock<Specification,
models::stellar::state::SurfaceShape> {
using Type = utils::blocks::surface_deformation::parameters::value;
static constexpr bool mapped = true;
};
template <models::ModelSpecification Specification>
struct StellarDependencyBlock<Specification,
models::stellar::state::GravityGradient> {
using Type = utils::blocks::gravity::gradient::value;
static constexpr bool mapped = true;
};
template <models::ModelSpecification Specification>
struct StellarDependencyBlock<
Specification, models::stellar::state::GravitationalPotential> {
using Type = utils::blocks::gravity::poisson::value;
static constexpr bool mapped = true;
};
template <models::ModelSpecification Specification>
struct StellarDependencyBlock<Specification,
models::stellar::state::SpecificEnthalpy> {
using Type = utils::blocks::enthalpy::specific::value;
static constexpr bool mapped = true;
};
template <models::ModelSpecification Specification>
struct StellarDependencyBlock<
Specification, models::stellar::state::OwnGeneratedCoordinate> {
private:
using GeneratedBlocks = typename GeneratedValueBlocksFor<
typename models::SpecificationContribution<
Specification>::GeneratedValues>::Type;
using Selection = SingleGeneratedBlock<
GeneratedBlocks, models::stellar::state::OwnGeneratedCoordinate>;
public:
using Type = typename Selection::Type;
static constexpr bool mapped = Selection::available;
};
template <models::ModelSpecification Specification,
models::ModelSpecification Owner>
struct StellarDependencyBlock<
Specification, models::stellar::state::GeneratedCoordinateOf<Owner>> {
private:
using GeneratedBlocks = typename GeneratedValueBlocksFor<
typename models::SpecificationContribution<Owner>::GeneratedValues>::Type;
using Dependency = models::stellar::state::GeneratedCoordinateOf<Owner>;
using Selection = SingleGeneratedBlock<GeneratedBlocks, Dependency>;
public:
using Type = typename Selection::Type;
static constexpr bool mapped = Selection::available;
};
template <models::ModelSpecification Specification>
struct StellarDependencyBlock<
Specification, models::stellar::equation::GravityGradientDefinition> {
using Type = utils::blocks::gravity::gradient::residual;
static constexpr bool mapped = true;
};
template <models::ModelSpecification Specification>
struct StellarDependencyBlock<Specification,
models::stellar::equation::PoissonEquation> {
using Type = utils::blocks::gravity::poisson::residual;
static constexpr bool mapped = true;
};
template <models::ModelSpecification Specification>
struct StellarDependencyBlock<Specification,
models::stellar::equation::DensityClosure> {
using Type = utils::blocks::density::mass::residual;
static constexpr bool mapped = true;
};
template <models::ModelSpecification Specification>
struct StellarDependencyBlock<
Specification, models::stellar::equation::SurfaceShapeBalance> {
using Type =
utils::blocks::surface_deformation::shape_equilibrium::residual;
static constexpr bool mapped = true;
};
template <models::ModelSpecification Specification>
struct StellarDependencyBlock<
Specification, models::stellar::equation::HydrostaticBalance> {
using Type = utils::blocks::enthalpy::specific::residual;
static constexpr bool mapped = true;
};
template <models::ModelSpecification Specification>
struct StellarDependencyBlock<Specification,
models::stellar::equation::OwnConstraint> {
private:
using GeneratedBlocks = typename GeneratedResidualBlocksFor<
typename models::SpecificationContribution<
Specification>::GeneratedResiduals>::Type;
using Selection = SingleGeneratedBlock<
GeneratedBlocks, models::stellar::equation::OwnConstraint>;
public:
using Type = typename Selection::Type;
static constexpr bool mapped = Selection::available;
};
template <models::ModelSpecification Specification,
models::ModelSpecification Owner>
struct StellarDependencyBlock<
Specification, models::stellar::equation::ConstraintOf<Owner>> {
private:
using GeneratedBlocks = typename GeneratedResidualBlocksFor<
typename models::SpecificationContribution<Owner>::GeneratedResiduals>::Type;
using Dependency = models::stellar::equation::ConstraintOf<Owner>;
using Selection = SingleGeneratedBlock<GeneratedBlocks, Dependency>;
public:
using Type = typename Selection::Type;
static constexpr bool mapped = Selection::available;
};
template <models::ModelSpecification Specification, typename Dependencies>
struct CompileStellarDependencies {
using Type = utils::blocks::type_list<
UnmappedStellarDependency<Dependencies>>;
static constexpr bool complete = false;
};
template <models::ModelSpecification Specification, typename... Dependencies>
struct CompileStellarDependencies<Specification,
models::ModelTypeList<Dependencies...>> {
using Type = utils::blocks::type_list<
typename StellarDependencyBlock<Specification, Dependencies>::Type...>;
static constexpr bool complete =
(StellarDependencyBlock<Specification, Dependencies>::mapped && ...);
};
template <typename Residual, typename Values> struct CoupleResidualToValues;
template <typename Residual, typename... Values>
struct CoupleResidualToValues<Residual, utils::blocks::type_list<Values...>> {
using Type = utils::blocks::type_list<
StellarEquilibriumJacobianCoupling<Residual, Values>...>;
};
template <typename Residuals, typename Values>
struct CartesianJacobianCouplings;
template <typename... Residuals, typename Values>
struct CartesianJacobianCouplings<utils::blocks::type_list<Residuals...>,
Values> {
using Type = ConcatenateBlockListsT<
typename CoupleResidualToValues<Residuals, Values>::Type...>;
};
template <typename Candidate> struct IsJacobianCoupling : std::false_type {};
template <typename Residual, typename Value>
struct IsJacobianCoupling<StellarEquilibriumJacobianCoupling<Residual, Value>>
: std::bool_constant<
std::derived_from<Residual, utils::blocks::residual_block_base> &&
std::derived_from<Value, utils::blocks::value_block_base>> {};
template <typename Candidate>
struct IsJacobianCouplingList : std::false_type {};
template <typename... Couplings>
struct IsJacobianCouplingList<utils::blocks::type_list<Couplings...>>
: std::bool_constant<(IsJacobianCoupling<Couplings>::value && ...) &&
utils::blocks::types_are_unique_v<
utils::blocks::type_list<Couplings...>>> {};
template <typename Candidate> struct IsGeneratedValueBlock : std::false_type {};
template <typename Owner>
struct IsGeneratedValueBlock<utils::blocks::generated_value_block<Owner>>
: std::true_type {};
template <typename Candidate>
struct IsGeneratedResidualBlock : std::false_type {};
template <typename Owner>
struct IsGeneratedResidualBlock<utils::blocks::generated_residual_block<Owner>>
: std::true_type {};
template <typename Coupling>
inline constexpr bool isGeneratedBorderIncidentCoupling =
IsGeneratedValueBlock<typename Coupling::Value>::value ||
IsGeneratedResidualBlock<typename Coupling::Residual>::value;
template <typename Couplings> struct GeneratedBorderIncidentCouplings;
template <>
struct GeneratedBorderIncidentCouplings<utils::blocks::type_list<>> {
using Type = utils::blocks::type_list<>;
};
template <typename Head, typename... Tail>
struct GeneratedBorderIncidentCouplings<
utils::blocks::type_list<Head, Tail...>> {
private:
using Remaining = typename GeneratedBorderIncidentCouplings<
utils::blocks::type_list<Tail...>>::Type;
public:
using Type = std::conditional_t<
isGeneratedBorderIncidentCoupling<Head>,
ConcatenateBlockListsT<utils::blocks::type_list<Head>, Remaining>,
Remaining>;
};
template <bool Registered, typename GeneratedValues,
typename GeneratedResiduals, typename DependsOn, typename Affects>
struct DeclarativeStellarEquilibriumSpecificationCompilation {
using GeneratedValueBlocks = GeneratedValues;
using GeneratedResidualBlocks = GeneratedResiduals;
using DependsOnValueBlocks = DependsOn;
using AffectedResidualBlocks = Affects;
/*
* Preserve the two physical meanings in the declaration instead of
* flattening their endpoints into independent unions:
*
* constraint equation <- everything named in Reads
* changed equations <- Reads plus the generated coordinate
*
* The second group deliberately includes Affects x Reads. Nonlinear
* constraints and multiplier forces generally contribute Hessian-like
* state derivatives there. Linear contributions simply assemble zero on
* those structurally permitted edges.
*/
using ConstraintInputValueBlocks = DependsOnValueBlocks;
using ConstraintOutputResidualBlocks = GeneratedResidualBlocks;
using ChangedEquationInputValueBlocks = UniqueConcatenateBlockListsT<
DependsOnValueBlocks, GeneratedValueBlocks>;
using ChangedEquationOutputResidualBlocks = AffectedResidualBlocks;
using ConstraintJacobianCouplings =
typename CartesianJacobianCouplings<GeneratedResidualBlocks,
DependsOnValueBlocks>::Type;
using ChangedEquationJacobianCouplings =
typename CartesianJacobianCouplings<AffectedResidualBlocks,
ChangedEquationInputValueBlocks>::Type;
// Compatibility names retained for backend code that distinguishes the
// generated row from the generated-coordinate column.
using GeneratedRowJacobianCouplings = ConstraintJacobianCouplings;
using AffectedRowJacobianCouplings =
typename CartesianJacobianCouplings<AffectedResidualBlocks,
GeneratedValueBlocks>::Type;
using AffectedStateJacobianCouplings =
typename CartesianJacobianCouplings<AffectedResidualBlocks,
DependsOnValueBlocks>::Type;
using JacobianCouplings = UniqueConcatenateBlockListsT<
ConstraintJacobianCouplings, ChangedEquationJacobianCouplings>;
using IncidentJacobianCouplings =
typename GeneratedBorderIncidentCouplings<JacobianCouplings>::Type;
// Correction is the Newton-facing name for a value coordinate.
using GeneratedCorrectionBlocks = GeneratedValueBlocks;
static constexpr bool registered = Registered;
static constexpr bool complete =
registered && IsValueBlockList<GeneratedValueBlocks>::value &&
IsResidualBlockList<GeneratedResidualBlocks>::value &&
IsValueBlockList<DependsOnValueBlocks>::value &&
IsResidualBlockList<AffectedResidualBlocks>::value &&
IsJacobianCouplingList<JacobianCouplings>::value &&
(GeneratedValueBlocks::size == GeneratedResidualBlocks::size) &&
((GeneratedValueBlocks::size == 0 && DependsOnValueBlocks::size == 0 &&
AffectedResidualBlocks::size == 0) ||
(GeneratedValueBlocks::size > 0 && DependsOnValueBlocks::size > 0 &&
AffectedResidualBlocks::size > 0));
};
using EmptySpecificationCompilation =
DeclarativeStellarEquilibriumSpecificationCompilation<
false, utils::blocks::type_list<>, utils::blocks::type_list<>,
utils::blocks::type_list<>, utils::blocks::type_list<>>;
template <models::ModelSpecification Specification>
struct SelfDescribingSpecificationCompilationInputs {
using Contribution = models::SpecificationContribution<Specification>;
using DependsOn =
CompileStellarDependencies<Specification, typename Contribution::DependsOn>;
using Affects =
CompileStellarDependencies<Specification, typename Contribution::Affects>;
using GeneratedValueBlocks = typename GeneratedValueBlocksFor<
typename Contribution::GeneratedValues>::Type;
using GeneratedResidualBlocks = typename GeneratedResidualBlocksFor<
typename Contribution::GeneratedResiduals>::Type;
using DependsOnValueBlocks = typename DependsOn::Type;
using AffectedResidualBlocks = typename Affects::Type;
static constexpr bool registered =
Contribution::hasDeclarativeDefinition && DependsOn::complete &&
Affects::complete;
};
template <models::ModelSpecification Specification>
struct SelfDescribingSpecificationCompilation
: DeclarativeStellarEquilibriumSpecificationCompilation<
SelfDescribingSpecificationCompilationInputs<Specification>::registered,
typename SelfDescribingSpecificationCompilationInputs<
Specification>::GeneratedValueBlocks,
typename SelfDescribingSpecificationCompilationInputs<
Specification>::GeneratedResidualBlocks,
typename SelfDescribingSpecificationCompilationInputs<
Specification>::DependsOnValueBlocks,
typename SelfDescribingSpecificationCompilationInputs<
Specification>::AffectedResidualBlocks> {};
} // namespace detail
/*
* Public, inspectable per-specification compilation metadata. The primary
* is deliberately well formed and incomplete, so testing an arbitrary type
* in a requires-expression never triggers a diagnostic.
*/
template <typename Specification>
struct StellarEquilibriumSpecificationCompilation
: detail::EmptySpecificationCompilation {};
template <models::ModelSpecification Specification>
struct StellarEquilibriumSpecificationCompilation<Specification>
: detail::SelfDescribingSpecificationCompilation<Specification> {};
namespace detail {
template <typename Specification, typename = void>
struct SpecificationCompilationIsComplete : std::false_type {};
template <typename Specification>
struct SpecificationCompilationIsComplete<
Specification,
std::void_t<typename StellarEquilibriumSpecificationCompilation<
Specification>::GeneratedValueBlocks,
typename StellarEquilibriumSpecificationCompilation<
Specification>::GeneratedResidualBlocks,
typename StellarEquilibriumSpecificationCompilation<
Specification>::DependsOnValueBlocks,
typename StellarEquilibriumSpecificationCompilation<
Specification>::AffectedResidualBlocks,
typename StellarEquilibriumSpecificationCompilation<
Specification>::JacobianCouplings,
std::bool_constant<StellarEquilibriumSpecificationCompilation<
Specification>::registered>,
std::bool_constant<StellarEquilibriumSpecificationCompilation<
Specification>::complete>>>
: std::bool_constant<
StellarEquilibriumSpecificationCompilation<
Specification>::registered &&
StellarEquilibriumSpecificationCompilation<Specification>::complete &&
IsValueBlockList<typename StellarEquilibriumSpecificationCompilation<
Specification>::GeneratedValueBlocks>::value &&
IsResidualBlockList<
typename StellarEquilibriumSpecificationCompilation<
Specification>::GeneratedResidualBlocks>::value &&
IsValueBlockList<typename StellarEquilibriumSpecificationCompilation<
Specification>::DependsOnValueBlocks>::value &&
IsResidualBlockList<
typename StellarEquilibriumSpecificationCompilation<
Specification>::AffectedResidualBlocks>::value &&
IsJacobianCouplingList<
typename StellarEquilibriumSpecificationCompilation<
Specification>::JacobianCouplings>::value> {};
} // namespace detail
template <typename Candidate>
inline constexpr bool stellarEquilibriumSpecificationCompilationComplete =
detail::SpecificationCompilationIsComplete<
std::remove_cvref_t<Candidate>>::value;
template <typename Candidate>
concept StellarEquilibriumSpecificationCompilable =
stellarEquilibriumSpecificationCompilationComplete<Candidate>;
namespace detail {
/*
* This five-by-five physical core is independent of global constraints.
* Even FixedTotalMass is compiled as a contribution, keeping C and R_M
* visible in that specification's metadata.
*/
using StellarPhysicsValueBlocks = utils::blocks::type_list<
utils::blocks::density::mass::value,
utils::blocks::surface_deformation::parameters::value,
utils::blocks::gravity::gradient::value,
utils::blocks::gravity::poisson::value,
utils::blocks::enthalpy::specific::value>;
using StellarPhysicsResidualBlocks = utils::blocks::type_list<
utils::blocks::gravity::gradient::residual,
utils::blocks::gravity::poisson::residual,
utils::blocks::density::mass::residual,
utils::blocks::surface_deformation::shape_equilibrium::residual,
utils::blocks::enthalpy::specific::residual>;
using StellarPhysicsJacobianRows = utils::blocks::type_list<
utils::blocks::block_row<
utils::blocks::gravity::gradient::residual,
utils::blocks::gravity::gradient::value,
utils::blocks::gravity::poisson::value,
utils::blocks::surface_deformation::parameters::value>,
utils::blocks::block_row<
utils::blocks::gravity::poisson::residual,
utils::blocks::gravity::gradient::value,
utils::blocks::density::mass::value,
utils::blocks::surface_deformation::parameters::value>,
utils::blocks::block_row<
utils::blocks::density::mass::residual,
utils::blocks::density::mass::value,
utils::blocks::enthalpy::specific::value,
utils::blocks::surface_deformation::parameters::value>,
utils::blocks::block_row<
utils::blocks::surface_deformation::shape_equilibrium::residual,
utils::blocks::density::mass::value,
utils::blocks::surface_deformation::parameters::value,
utils::blocks::gravity::gradient::value,
utils::blocks::enthalpy::specific::value>,
utils::blocks::block_row<
utils::blocks::enthalpy::specific::residual,
utils::blocks::enthalpy::specific::value,
utils::blocks::gravity::poisson::value,
utils::blocks::surface_deformation::parameters::value>>;
template <typename Row> struct JacobianRowCouplings;
template <typename Residual, typename... Values>
struct JacobianRowCouplings<utils::blocks::block_row<Residual, Values...>> {
using Type = utils::blocks::type_list<
StellarEquilibriumJacobianCoupling<Residual, Values>...>;
};
template <typename Rows> struct FlattenJacobianRows;
template <typename... Rows>
struct FlattenJacobianRows<utils::blocks::type_list<Rows...>> {
using Type =
ConcatenateBlockListsT<typename JacobianRowCouplings<Rows>::Type...>;
};
using StellarPhysicsJacobianCouplings =
typename FlattenJacobianRows<StellarPhysicsJacobianRows>::Type;
template <typename SpecificationSet>
struct SpecificationSetCompilationsAreComplete;
template <models::ModelSpecification... Specifications>
struct SpecificationSetCompilationsAreComplete<
models::detail::SpecificationSetStorage<Specifications...>>
: std::bool_constant<(
stellarEquilibriumSpecificationCompilationComplete<Specifications> &&
...)> {};
template <typename SpecificationSet, bool Complete>
struct CollectStellarEquilibriumContributionsImpl {
using GeneratedValueBlocks = utils::blocks::type_list<>;
using GeneratedResidualBlocks = utils::blocks::type_list<>;
using ContributionJacobianCouplings = utils::blocks::type_list<>;
using IncidentJacobianCouplings = ContributionJacobianCouplings;
static constexpr bool complete = false;
};
template <models::ModelSpecification... Specifications>
struct CollectStellarEquilibriumContributionsImpl<
models::detail::SpecificationSetStorage<Specifications...>, true> {
using GeneratedValueBlocks = ConcatenateBlockListsT<
typename StellarEquilibriumSpecificationCompilation<
Specifications>::GeneratedValueBlocks...>;
using GeneratedResidualBlocks = ConcatenateBlockListsT<
typename StellarEquilibriumSpecificationCompilation<
Specifications>::GeneratedResidualBlocks...>;
using ContributionJacobianCouplings = UniqueConcatenateBlockListsT<
typename StellarEquilibriumSpecificationCompilation<
Specifications>::JacobianCouplings...>;
using IncidentJacobianCouplings = UniqueConcatenateBlockListsT<
typename StellarEquilibriumSpecificationCompilation<
Specifications>::IncidentJacobianCouplings...>;
static constexpr bool complete = true;
};
template <typename SpecificationSet>
using CollectStellarEquilibriumContributions =
CollectStellarEquilibriumContributionsImpl<
SpecificationSet,
SpecificationSetCompilationsAreComplete<SpecificationSet>::value>;
template <typename Residual, typename Couplings> struct ValuesCoupledToResidual;
template <typename Residual>
struct ValuesCoupledToResidual<Residual, utils::blocks::type_list<>> {
using Type = utils::blocks::type_list<>;
};
template <typename Residual, typename HeadResidual, typename HeadValue,
typename... Tail>
struct ValuesCoupledToResidual<
Residual,
utils::blocks::type_list<
StellarEquilibriumJacobianCoupling<HeadResidual, HeadValue>, Tail...>> {
private:
using Remaining =
typename ValuesCoupledToResidual<Residual,
utils::blocks::type_list<Tail...>>::Type;
public:
using Type = std::conditional_t<
std::same_as<Residual, HeadResidual>,
ConcatenateBlockListsT<utils::blocks::type_list<HeadValue>, Remaining>,
Remaining>;
};
template <typename Residual, typename Values> struct MakeJacobianRow;
template <typename Residual, typename... Values>
struct MakeJacobianRow<Residual, utils::blocks::type_list<Values...>> {
using Type = utils::blocks::block_row<Residual, Values...>;
};
template <typename Residuals, typename Couplings> struct SynthesizeJacobianRows;
template <typename... Residuals, typename Couplings>
struct SynthesizeJacobianRows<utils::blocks::type_list<Residuals...>,
Couplings> {
using Type = utils::blocks::type_list<typename MakeJacobianRow<
Residuals,
typename ValuesCoupledToResidual<Residuals, Couplings>::Type>::Type...>;
};
template <typename Couplings, typename ValueBlocks, typename ResidualBlocks>
struct CouplingEndpointsBelongToForm : std::false_type {};
template <typename ValueBlocks, typename ResidualBlocks, typename... Couplings>
struct CouplingEndpointsBelongToForm<utils::blocks::type_list<Couplings...>,
ValueBlocks, ResidualBlocks>
: std::bool_constant<((utils::blocks::contains_type_v<
typename Couplings::Value, ValueBlocks> &&
utils::blocks::contains_type_v<
typename Couplings::Residual, ResidualBlocks>) &&
...)> {};
template <typename Candidate> struct CompileStellarEquilibriumSystem {
using GeneratedValueBlocks = utils::blocks::type_list<>;
using GeneratedCorrectionBlocks = GeneratedValueBlocks;
using GeneratedResidualBlocks = utils::blocks::type_list<>;
using BaseJacobianCouplings = utils::blocks::type_list<>;
using ContributionJacobianCouplings = utils::blocks::type_list<>;
using IncidentJacobianCouplings = ContributionJacobianCouplings;
using JacobianCouplings = utils::blocks::type_list<>;
static constexpr bool compilable = false;
};
template <model::StellarModelType Model>
struct CompileStellarEquilibriumSystem<Model> {
using ModelType = std::remove_cvref_t<Model>;
using Contributions = CollectStellarEquilibriumContributions<
typename ModelType::SpecificationTypes>;
using GeneratedValueBlocks = typename Contributions::GeneratedValueBlocks;
using GeneratedCorrectionBlocks = GeneratedValueBlocks;
using GeneratedResidualBlocks =
typename Contributions::GeneratedResidualBlocks;
using ValueBlocks =
ConcatenateBlockListsT<StellarPhysicsValueBlocks, GeneratedValueBlocks>;
using ResidualBlocks = ConcatenateBlockListsT<StellarPhysicsResidualBlocks,
GeneratedResidualBlocks>;
using FormType = utils::blocks::block_form<ValueBlocks, ResidualBlocks>;
using BaseJacobianCouplings = StellarPhysicsJacobianCouplings;
using ContributionJacobianCouplings =
typename Contributions::ContributionJacobianCouplings;
using IncidentJacobianCouplings = ContributionJacobianCouplings;
using JacobianCouplings =
UniqueConcatenateBlockListsT<BaseJacobianCouplings,
ContributionJacobianCouplings>;
// Pass two: materialize rows only after all contributed blocks are
// present in the final form.
using JacobianType =
typename SynthesizeJacobianRows<ResidualBlocks, JacobianCouplings>::Type;
static constexpr bool compilable =
Contributions::complete &&
utils::blocks::block_form_is_valid_v<FormType> &&
IsJacobianCouplingList<JacobianCouplings>::value &&
CouplingEndpointsBelongToForm<JacobianCouplings, ValueBlocks,
ResidualBlocks>::value &&
utils::blocks::jacobian_form_is_valid_v<FormType, JacobianType>;
};
} // namespace detail
template <typename Candidate>
inline constexpr bool stellarEquilibriumSystemIsCompilable =
detail::CompileStellarEquilibriumSystem<
std::remove_cvref_t<Candidate>>::compilable;
/*
* This compiler proves the symbolic block topology only. Keep the explicit
* name available to extension authors and tests so that success here is not
* mistaken for an assembled numerical runtime. The established spelling is
* retained below as a compatibility alias.
*/
template <typename Candidate>
inline constexpr bool stellarEquilibriumIsSymbolicallyCompilable =
stellarEquilibriumSystemIsCompilable<Candidate>;
template <typename Candidate>
concept StellarEquilibriumSymbolicallyCompilable =
stellarEquilibriumIsSymbolicallyCompilable<Candidate>;
template <typename Candidate>
concept StellarEquilibriumSystemCompilable =
StellarEquilibriumSymbolicallyCompilable<Candidate>;
template <model::StellarModelType Model>
requires StellarEquilibriumSystemCompilable<Model>
struct CompiledStellarEquilibriumSystem final
: detail::CompileStellarEquilibriumSystem<std::remove_cvref_t<Model>> {
using Base =
detail::CompileStellarEquilibriumSystem<std::remove_cvref_t<Model>>;
using FormType = typename Base::FormType;
using JacobianType = typename Base::JacobianType;
// This classification is exposed only after the complete compiler concept
// has succeeded; model declarations intentionally do not predict it.
static constexpr models::EquilibriumSystemCompilation compilationClass =
models::EquilibriumSystemCompilation::complete_equilibrium_system;
static_assert(utils::blocks::block_form_is_valid_v<FormType>);
static_assert(utils::blocks::valid_jacobian_form<FormType, JacobianType>);
};
template <model::StellarModelType Model>
requires StellarEquilibriumSystemCompilable<Model>
using CompiledStellarEquilibriumForm =
typename CompiledStellarEquilibriumSystem<Model>::FormType;
template <model::StellarModelType Model>
requires StellarEquilibriumSystemCompilable<Model>
using CompiledStellarEquilibriumJacobianForm =
typename CompiledStellarEquilibriumSystem<Model>::JacobianType;
} // namespace mean_field::operators

View File

@@ -2,6 +2,8 @@ module;
#include <concepts> #include <concepts>
#include <cstddef> #include <cstddef>
#include <cstdint>
#include <memory>
#include <type_traits> #include <type_traits>
#include <utility> #include <utility>
@@ -13,46 +15,126 @@ export import :deformation.domain_deformation;
export import :equilibrium.stellar_discretization; export import :equilibrium.stellar_discretization;
export import :material.thermodynamic_equations; export import :material.thermodynamic_equations;
export import :model.typed_stellar; export import :model.typed_stellar;
export import :operators.prepared_central_density_stellar_equilibrium; export import :normalization.operators;
export import :operators.prepared_variadic_stellar_equilibrium;
export import :surface.compiler; export import :surface.compiler;
export namespace mean_field::equilibrium { export namespace mean_field::equilibrium {
template <typename Candidate> namespace detail {
concept StellarEquilibriumModel = model::StellarModelType<Candidate> && requires { template <
requires std::remove_cvref_t<Candidate>::template containsSpecification<eos::Polytrope>; model::StellarModelType Model,
requires std::remove_cvref_t<Candidate>::template containsSpecification<surface::Isobaric>; bool SymbolicallyCompilable = operators::StellarEquilibriumSystemCompilable<Model>>
requires std::remove_cvref_t<Candidate>::template containsSpecification<models::FixedTotalMass>; struct StellarSurfaceCompilationAudit {
requires std::remove_cvref_t<Candidate>::specificationCount == static constexpr bool complete = false;
3 + static_cast<std::size_t>(
std::remove_cvref_t<Candidate>::template containsSpecification<models::FixedCentralDensity>
);
}; };
template <StellarEquilibriumModel Model> class StellarEquilibriumProblem final { template <model::StellarModelType Model>
struct StellarSurfaceCompilationAudit<Model, true> {
private:
using ModelType = std::remove_cvref_t<Model>;
using EquationOfState = typename ModelType::EquationOfStateType;
using Form = operators::CompiledStellarEquilibriumForm<ModelType>;
using AvailableEquations = material::StellarEquilibriumThermodynamicEquations;
static constexpr bool thermodynamicsCompilable =
material::ThermodynamicEquationsCompilable<EquationOfState, Form, AvailableEquations>;
public:
static constexpr bool complete = [] {
if constexpr (!thermodynamicsCompilable) {
return false;
} else {
using ThermodynamicEquations =
material::CompiledThermodynamicEquationsT<EquationOfState, Form, AvailableEquations>;
using Formulation = typename ThermodynamicEquations::PressureSurfaceFormulation;
using CompiledSurface =
surface::CompiledPressureSurfaceConstraintT<Formulation, EquationOfState>;
return requires(const ModelType &model) {
{
surface::compilePressureSurfaceConstraint<Formulation>(
model.surfaceCondition(),
model.equationOfState()
)
} -> std::same_as<CompiledSurface>;
};
}
}();
};
} // namespace detail
template <model::StellarModelType Model>
inline constexpr bool hasStellarEquilibriumSurfaceCompilation =
detail::StellarSurfaceCompilationAudit<std::remove_cvref_t<Model>>::complete;
template <typename Candidate>
concept StellarEquilibriumModel = model::StellarModelType<Candidate> && requires {
typename std::remove_cvref_t<Candidate>::EquationOfStateType;
requires(
std::remove_cvref_t<Candidate>::template specificationRoleCount<
models::SpecificationRole::boundary_condition> == 1
);
requires std::remove_cvref_t<Candidate>::template containsSpecification<models::FixedTotalMass>;
requires operators::StellarEquilibriumSystemCompilable<std::remove_cvref_t<Candidate>>;
requires hasStellarEquilibriumSurfaceCompilation<std::remove_cvref_t<Candidate>>;
requires operators::CompilableRootManifestFor<
std::remove_cvref_t<Candidate>,
operators::CompiledStellarEquilibriumForm<std::remove_cvref_t<Candidate>>>;
requires operators::hasStellarEquilibriumCoreRuntime<std::remove_cvref_t<Candidate>>;
requires operators::hasCompleteStellarEquilibriumRuntime<std::remove_cvref_t<Candidate>>;
requires operators::stellarEquilibriumRotationProviderCount<std::remove_cvref_t<Candidate>> <= 1;
};
namespace detail {
template <typename Model, typename Discretization, typename = void>
struct StellarEquilibriumModelDiscretizationStructureAudit : std::false_type { };
template <typename Model, typename Discretization>
requires StellarEquilibriumModel<std::remove_cvref_t<Model>> &&
StellarDiscretizationType<std::remove_cvref_t<Discretization>>
struct StellarEquilibriumModelDiscretizationStructureAudit<
Model,
Discretization,
std::void_t<
typename std::remove_cvref_t<Discretization>::NormalizationPrescriptionType,
typename std::remove_cvref_t<Model>::SpecificationTypes,
operators::CompiledStellarEquilibriumForm<std::remove_cvref_t<Model>>,
operators::StellarEquilibriumPhysicalCoreType<std::remove_cvref_t<Model>>>>
: std::bool_constant<normalization::StellarNormalizationRuntimeAvailableFor<
typename std::remove_cvref_t<Discretization>::NormalizationPrescriptionType,
operators::CompiledStellarEquilibriumForm<std::remove_cvref_t<Model>>,
operators::StellarEquilibriumPhysicalCoreType<std::remove_cvref_t<Model>>,
typename std::remove_cvref_t<Model>::SpecificationTypes>> { };
struct StellarEquilibriumProblemFactory;
} // namespace detail
template <
StellarEquilibriumModel Model,
StellarDiscretizationType Discretization = StellarDiscretization>
requires detail::StellarEquilibriumModelDiscretizationStructureAudit<
std::remove_cvref_t<Model>,
std::remove_cvref_t<Discretization>>::value
class StellarEquilibriumProblem final {
public: public:
using ModelType = std::remove_cvref_t<Model>; using ModelType = std::remove_cvref_t<Model>;
using DiscretizationType = std::remove_cvref_t<Discretization>;
using NormalizationPrescriptionType = typename DiscretizationType::NormalizationPrescriptionType;
static constexpr bool hasFixedCentralDensity = static constexpr bool hasFixedCentralDensity =
ModelType::template containsSpecification<models::FixedCentralDensity>; ModelType::template containsSpecification<models::FixedCentralDensity>;
static constexpr bool hasFixedAngularMomentum =
ModelType::template containsSpecification<models::FixedAngularMomentum>;
static constexpr std::size_t generatedRotationProviderCount =
operators::stellarEquilibriumRotationProviderCount<ModelType>;
static constexpr bool symbolicallySquare = ModelType::symbolicallySquare; static constexpr bool symbolicallySquare = ModelType::symbolicallySquare;
using PreparedOperatorType = std::conditional_t< using PreparedOperatorType = operators::PreparedVariadicStellarEquilibriumOperator<ModelType>;
hasFixedCentralDensity, using PhysicalCoreType = typename PreparedOperatorType::PhysicalCoreType;
operators::PreparedCentralDensityStellarEquilibriumOperator, using FormType = operators::CompiledStellarEquilibriumForm<ModelType>;
operators::PreparedStellarEquilibriumOperator>; using JacobianFormType = operators::CompiledStellarEquilibriumJacobianForm<ModelType>;
using FormType = std::conditional_t< using ManifestType = operators::EquilibriumSystemManifest<ModelType, FormType, JacobianFormType>;
hasFixedCentralDensity, using EquationOfStateType = model::EquationOfStateType<ModelType>;
operators::CentralDensityStellarEquilibriumForm, using SurfaceConditionType = model::SurfaceConditionType<ModelType>;
utils::blocks::surface_deformed_stellar_equilibrium_form>;
using JacobianFormType = std::conditional_t<
hasFixedCentralDensity,
operators::CentralDensityStellarEquilibriumJacobianForm,
utils::blocks::surface_deformed_stellar_equilibrium_jacobian_form>;
using ManifestType = std::conditional_t<
hasFixedCentralDensity,
operators::CentralDensityStellarEquilibriumSystemManifest,
operators::StellarEquilibriumSystemManifest>;
using EquationOfStateType = eos::Polytrope;
using AvailableThermodynamicEquations = material::StellarEquilibriumThermodynamicEquations; using AvailableThermodynamicEquations = material::StellarEquilibriumThermodynamicEquations;
using ThermodynamicEquationsType = using ThermodynamicEquationsType =
material::CompiledThermodynamicEquationsT<EquationOfStateType, FormType, AvailableThermodynamicEquations>; material::CompiledThermodynamicEquationsT<EquationOfStateType, FormType, AvailableThermodynamicEquations>;
@@ -60,44 +142,22 @@ export namespace mean_field::equilibrium {
typename ThermodynamicEquationsType::PressureSurfaceFormulation, typename ThermodynamicEquationsType::PressureSurfaceFormulation,
EquationOfStateType>; EquationOfStateType>;
StellarEquilibriumProblem( private:
ModelType stellarModel, friend struct detail::StellarEquilibriumProblemFactory;
const StellarDiscretization discretization
)
requires(!hasFixedCentralDensity)
: m_stellarModel(std::move(stellarModel)),
m_discretization(discretization),
m_compiledSurfaceConstraint(CompileSurfaceConstraint(m_stellarModel)),
m_preparedOperator(
m_discretization.finiteElementModel(),
m_discretization.domainMapper(),
m_stellarModel.template specification<eos::Polytrope>(),
models::compileConstraint(m_stellarModel.template specification<models::FixedTotalMass>()),
operators::PressureSurfaceConstraintView{m_compiledSurfaceConstraint},
CompileDefaultDomainDeformation(m_discretization.finiteElementModel())
) {
VerifyProblem();
}
StellarEquilibriumProblem( StellarEquilibriumProblem(
ModelType stellarModel, ModelType stellarModel,
const StellarDiscretization discretization DiscretizationType discretization
) )
requires hasFixedCentralDensity : m_stellarModel(std::make_shared<ModelType>(std::move(stellarModel))),
: m_stellarModel(std::move(stellarModel)), m_discretization(std::move(discretization)),
m_discretization(discretization), m_compiledSurfaceConstraint(CompileSurfaceConstraint(*m_stellarModel)),
m_compiledSurfaceConstraint(CompileSurfaceConstraint(m_stellarModel)),
m_preparedOperator( m_preparedOperator(
m_discretization.finiteElementModel(), m_discretization.finiteElementModel(),
m_discretization.domainMapper(), m_discretization.domainMapper(),
m_stellarModel.template specification<eos::Polytrope>(), m_stellarModel,
models::compileConstraint(m_stellarModel.template specification<models::FixedTotalMass>()),
operators::PressureSurfaceConstraintView{m_compiledSurfaceConstraint}, operators::PressureSurfaceConstraintView{m_compiledSurfaceConstraint},
CompileDefaultDomainDeformation(m_discretization.finiteElementModel()), CompileDefaultDomainDeformation(m_discretization.finiteElementModel())
models::compileConstraint(
m_stellarModel.template specification<models::FixedCentralDensity>(),
m_stellarModel.template specification<eos::Polytrope>()
)
) { ) {
VerifyProblem(); VerifyProblem();
} }
@@ -107,14 +167,19 @@ export namespace mean_field::equilibrium {
StellarEquilibriumProblem(StellarEquilibriumProblem &&) = delete; StellarEquilibriumProblem(StellarEquilibriumProblem &&) = delete;
StellarEquilibriumProblem &operator=(StellarEquilibriumProblem &&) = delete; StellarEquilibriumProblem &operator=(StellarEquilibriumProblem &&) = delete;
public:
[[nodiscard]] const ModelType &GetStellarModel() const noexcept { [[nodiscard]] const ModelType &GetStellarModel() const noexcept {
return m_stellarModel; return *m_stellarModel;
} }
[[nodiscard]] const StellarDiscretization &GetDiscretization() const noexcept { [[nodiscard]] const DiscretizationType &GetDiscretization() const noexcept {
return m_discretization; return m_discretization;
} }
[[nodiscard]] const NormalizationPrescriptionType &GetNormalizationPrescription() const noexcept {
return m_discretization.normalizationPrescription();
}
[[nodiscard]] const CompiledSurfaceConstraintType &GetCompiledSurfaceConstraint() const noexcept { [[nodiscard]] const CompiledSurfaceConstraintType &GetCompiledSurfaceConstraint() const noexcept {
return m_compiledSurfaceConstraint; return m_compiledSurfaceConstraint;
} }
@@ -127,6 +192,10 @@ export namespace mean_field::equilibrium {
return m_preparedOperator; return m_preparedOperator;
} }
[[nodiscard]] const PhysicalCoreType &GetPhysicalOperator() const noexcept {
return m_preparedOperator.GetPhysicalOperator();
}
[[nodiscard]] const auto &GetManifest() const noexcept { [[nodiscard]] const auto &GetManifest() const noexcept {
return m_preparedOperator.GetRootManifest(); return m_preparedOperator.GetRootManifest();
} }
@@ -135,28 +204,20 @@ export namespace mean_field::equilibrium {
return m_preparedOperator.IsPrepared(); return m_preparedOperator.IsPrepared();
} }
[[nodiscard]] const operators::StellarEquilibriumDependencies &GetLinearizationDependencies() const { [[nodiscard]] std::uint64_t GetPreparationGeneration() const noexcept {
if constexpr (hasFixedCentralDensity) { return m_preparationGeneration;
return m_preparedOperator.GetPhysicalOperator().GetDependencies();
} else {
return m_preparedOperator.GetDependencies();
} }
[[nodiscard]] const operators::StellarEquilibriumDependencies &GetLinearizationDependencies() const {
return GetPhysicalOperator().GetDependencies();
} }
[[nodiscard]] const operators::StellarEquilibriumDependencyStamp &GetGeometryDependency() const { [[nodiscard]] const operators::StellarEquilibriumDependencyStamp &GetGeometryDependency() const {
if constexpr (hasFixedCentralDensity) { return GetPhysicalOperator().GetGeneratedDisplacementDependency();
return m_preparedOperator.GetPhysicalOperator().GetGeneratedDisplacementDependency();
} else {
return m_preparedOperator.GetGeneratedDisplacementDependency();
}
} }
[[nodiscard]] const field::FieldBoundaryDofMap &GetPressureSurfaceRows() const noexcept { [[nodiscard]] const field::FieldBoundaryDofMap &GetPressureSurfaceRows() const noexcept {
if constexpr (hasFixedCentralDensity) { return GetPhysicalOperator().GetSurfaceConstraintOperator().GetSurfaceRows();
return m_preparedOperator.GetPhysicalOperator().GetSurfaceConstraintOperator().GetSurfaceRows();
} else {
return m_preparedOperator.GetSurfaceConstraintOperator().GetSurfaceRows();
}
} }
[[nodiscard]] int StateSize() const noexcept { [[nodiscard]] int StateSize() const noexcept {
@@ -175,8 +236,19 @@ export namespace mean_field::equilibrium {
const mfem::Vector &state, const mfem::Vector &state,
const operators::StellarEquilibriumDependencies &dependencies, const operators::StellarEquilibriumDependencies &dependencies,
const physics::RigidRotation &rotation const physics::RigidRotation &rotation
) { ) requires(generatedRotationProviderCount == 0) {
return m_preparedOperator.Prepare(state, dependencies, rotation); auto report = m_preparedOperator.Prepare(state, dependencies, rotation);
++m_preparationGeneration;
return report;
}
[[nodiscard]] auto Prepare(
const mfem::Vector &state,
const operators::StellarEquilibriumDependencies &dependencies
) requires(generatedRotationProviderCount == 1) {
auto report = m_preparedOperator.Prepare(state, dependencies);
++m_preparationGeneration;
return report;
} }
void BuildResidual(mfem::Vector &residual) const { void BuildResidual(mfem::Vector &residual) const {
@@ -194,8 +266,8 @@ export namespace mean_field::equilibrium {
[[nodiscard]] static CompiledSurfaceConstraintType CompileSurfaceConstraint(const ModelType &stellarModel) { [[nodiscard]] static CompiledSurfaceConstraintType CompileSurfaceConstraint(const ModelType &stellarModel) {
return surface::compilePressureSurfaceConstraint< return surface::compilePressureSurfaceConstraint<
typename ThermodynamicEquationsType::PressureSurfaceFormulation>( typename ThermodynamicEquationsType::PressureSurfaceFormulation>(
stellarModel.template specification<surface::Isobaric>(), stellarModel.surfaceCondition(),
stellarModel.template specification<EquationOfStateType>() stellarModel.equationOfState()
); );
} }
@@ -224,34 +296,112 @@ export namespace mean_field::equilibrium {
MFEM_VERIFY(m_discretization.isCurrent(), "The stellar equilibrium problem has a stale discretization."); MFEM_VERIFY(m_discretization.isCurrent(), "The stellar equilibrium problem has a stale discretization.");
} }
ModelType m_stellarModel; std::shared_ptr<const ModelType> m_stellarModel;
StellarDiscretization m_discretization; DiscretizationType m_discretization;
CompiledSurfaceConstraintType m_compiledSurfaceConstraint; CompiledSurfaceConstraintType m_compiledSurfaceConstraint;
PreparedOperatorType m_preparedOperator; PreparedOperatorType m_preparedOperator;
std::uint64_t m_preparationGeneration{0};
}; };
template <StellarEquilibriumModel Model> template <typename Candidate> struct IsStellarEquilibriumProblem : std::false_type { };
[[nodiscard]] auto discretize(
template <StellarEquilibriumModel Model, StellarDiscretizationType Discretization>
requires detail::StellarEquilibriumModelDiscretizationStructureAudit<
std::remove_cvref_t<Model>,
std::remove_cvref_t<Discretization>>::value
struct IsStellarEquilibriumProblem<StellarEquilibriumProblem<Model, Discretization>> : std::true_type { };
template <typename Candidate>
concept DiscretizedStellarEquilibriumProblem = IsStellarEquilibriumProblem<std::remove_cvref_t<Candidate>>::value;
namespace detail {
template <
typename Model,
typename Discretization,
bool StructurallyCompatible =
StellarEquilibriumModelDiscretizationStructureAudit<
std::remove_cvref_t<Model>,
std::remove_cvref_t<Discretization>>::value>
struct StellarEquilibriumModelDiscretizationOperationAudit : std::false_type { };
template <typename Model, typename Discretization>
struct StellarEquilibriumModelDiscretizationOperationAudit<
Model,
Discretization,
true> {
private:
using ModelType = std::remove_cvref_t<Model>;
using DiscretizationType = std::remove_cvref_t<Discretization>;
using Problem = StellarEquilibriumProblem<ModelType, DiscretizationType>;
using Prescription = typename DiscretizationType::NormalizationPrescriptionType;
public:
static constexpr bool value = [] {
if constexpr (
std::same_as<Prescription, normalization::Unnormalized> ||
normalization::PhysicalRieszDiagonalPrescription<Prescription>) {
return true;
} else {
return normalization::RuntimePreparedNormalizationOperation<Problem>;
}
}();
};
} // namespace detail
/*
* A model and a discretization are separate compile-time choices. Their
* pairing is valid only when the normalization plan covers the inferred
* form, the selected physical runtime supports it, and a third-party
* runtime policy provides its exact preparation operation. Keeping this
* as a detection-safe public factory boundary rejects incomplete policies
* at discretize(), before a solver-facing problem can be constructed.
*/
template <typename Model, typename Discretization>
concept StellarEquilibriumModelDiscretizationCompatible =
detail::StellarEquilibriumModelDiscretizationOperationAudit<
std::remove_cvref_t<Model>,
std::remove_cvref_t<Discretization>>::value;
namespace detail {
/* The structurally formed problem type is needed to probe the ADL
* operation without a recursive concept. Its constructor remains
* private, and this factory is the single construction authority after
* the complete public compatibility contract has succeeded. */
struct StellarEquilibriumProblemFactory final {
template <StellarEquilibriumModel Model, StellarDiscretizationType Discretization>
requires StellarEquilibriumModelDiscretizationCompatible<Model, Discretization>
[[nodiscard]] static auto Create(
Model &&stellarModel, Model &&stellarModel,
const StellarDiscretization discretization Discretization discretization
) { ) {
using ModelType = std::remove_cvref_t<Model>; using ModelType = std::remove_cvref_t<Model>;
return StellarEquilibriumProblem<ModelType>{std::forward<Model>(stellarModel), discretization}; using DiscretizationType = std::remove_cvref_t<Discretization>;
return StellarEquilibriumProblem<ModelType, DiscretizationType>{
std::forward<Model>(stellarModel),
std::move(discretization)
};
}
};
} // namespace detail
template <StellarEquilibriumModel Model, StellarDiscretizationType Discretization>
requires StellarEquilibriumModelDiscretizationCompatible<Model, Discretization>
[[nodiscard]] auto discretize(
Model &&stellarModel,
Discretization discretization
) {
return detail::StellarEquilibriumProblemFactory::Create(
std::forward<Model>(stellarModel),
std::move(discretization)
);
} }
template <StellarEquilibriumModel Model> template <StellarEquilibriumModel Model>
requires StellarEquilibriumModelDiscretizationCompatible<Model, StellarDiscretization>
[[nodiscard]] auto discretize( [[nodiscard]] auto discretize(
Model &&stellarModel, Model &&stellarModel,
fem::FEM &finiteElementModel fem::FEM &finiteElementModel
) { ) {
return discretize(std::forward<Model>(stellarModel), StellarDiscretization{finiteElementModel}); return discretize(std::forward<Model>(stellarModel), StellarDiscretization{finiteElementModel});
} }
template <typename Candidate> struct IsStellarEquilibriumProblem : std::false_type { };
template <StellarEquilibriumModel Model>
struct IsStellarEquilibriumProblem<StellarEquilibriumProblem<Model>> : std::true_type { };
template <typename Candidate>
concept DiscretizedStellarEquilibriumProblem = IsStellarEquilibriumProblem<std::remove_cvref_t<Candidate>>::value;
} // namespace mean_field::equilibrium } // namespace mean_field::equilibrium

View File

@@ -292,7 +292,8 @@ export namespace mean_field::preconditioning {
}; };
template <equilibrium::DiscretizedStellarEquilibriumProblem Problem, SpecificationBorderBlockType Block> template <equilibrium::DiscretizedStellarEquilibriumProblem Problem, SpecificationBorderBlockType Block>
requires EquilibriumCoordinateComponentFor<Block, typename std::remove_cvref_t<Problem>::FormType> requires EquilibriumCoordinateComponentFor<Block, typename std::remove_cvref_t<Problem>::FormType> &&
SpecificationBorderPreparableFor<Problem, Block>
class PreparedStellarPreconditioner final : public mfem::Solver { class PreparedStellarPreconditioner final : public mfem::Solver {
private: private:
using ProblemType = std::remove_cvref_t<Problem>; using ProblemType = std::remove_cvref_t<Problem>;
@@ -324,6 +325,9 @@ export namespace mean_field::preconditioning {
} }
} }
PreparedStellarPreconditioner(ProblemType &&, BlockType) = delete;
PreparedStellarPreconditioner(const ProblemType &&, BlockType) = delete;
PreparedStellarPreconditioner(const PreparedStellarPreconditioner &) = delete; PreparedStellarPreconditioner(const PreparedStellarPreconditioner &) = delete;
PreparedStellarPreconditioner &operator=(const PreparedStellarPreconditioner &) = delete; PreparedStellarPreconditioner &operator=(const PreparedStellarPreconditioner &) = delete;
PreparedStellarPreconditioner(PreparedStellarPreconditioner &&) = delete; PreparedStellarPreconditioner(PreparedStellarPreconditioner &&) = delete;
@@ -367,6 +371,10 @@ export namespace mean_field::preconditioning {
return m_grouped.GetBlock(); return m_grouped.GetBlock();
} }
[[nodiscard]] const ProblemType &GetProblem() const noexcept {
return m_grouped.GetProblem();
}
[[nodiscard]] const GroupedPreconditioner &GetGroupedPreconditioner() const noexcept { [[nodiscard]] const GroupedPreconditioner &GetGroupedPreconditioner() const noexcept {
return m_grouped; return m_grouped;
} }
@@ -392,11 +400,26 @@ export namespace mean_field::preconditioning {
SpecificationBorderBlockType Block> SpecificationBorderBlockType Block>
requires EquilibriumCoordinateComponentFor< requires EquilibriumCoordinateComponentFor<
Block, Block,
typename std::remove_cvref_t<Problem>::FormType> typename std::remove_cvref_t<Problem>::FormType> &&
SpecificationBorderPreparableFor<Problem, Block>
[[nodiscard]] auto prepare( [[nodiscard]] auto prepare(
const Problem &problem, const Problem &problem,
Block block Block block
) { ) {
return PreparedStellarPreconditioner<Problem, Block>{problem, std::move(block)}; return PreparedStellarPreconditioner<Problem, Block>{problem, std::move(block)};
} }
template <typename Problem, SpecificationBorderBlockType Block>
requires (!std::is_lvalue_reference_v<Problem>) &&
equilibrium::DiscretizedStellarEquilibriumProblem<std::remove_cvref_t<Problem>> &&
EquilibriumCoordinateComponentFor<
Block,
typename std::remove_cvref_t<Problem>::FormType> &&
SpecificationBorderPreparableFor<std::remove_cvref_t<Problem>, Block>
[[nodiscard]] auto prepare(
Problem &&,
Block
) -> PreparedStellarPreconditioner<
std::remove_cvref_t<Problem>,
std::remove_cvref_t<Block>> = delete;
} // namespace mean_field::preconditioning } // namespace mean_field::preconditioning

View File

@@ -231,10 +231,90 @@ export namespace mean_field::preconditioning {
template <typename Candidate> template <typename Candidate>
concept MaterialSurfaceDescriptor = detail::IsMaterialSurfaceDescriptor<std::remove_cvref_t<Candidate>>::value; concept MaterialSurfaceDescriptor = detail::IsMaterialSurfaceDescriptor<std::remove_cvref_t<Candidate>>::value;
/*
* Capability boundary for EOS-specific material/surface surrogate
* assembly. The current kernels remain polytropic, but selection no
* longer embeds that closed-world type test in the descriptor concept.
*/
template <typename EquationOfState>
struct MaterialSurfaceEquationOfStateBackend {
static constexpr bool registered = false;
};
template <>
struct MaterialSurfaceEquationOfStateBackend<eos::Polytrope> {
static constexpr bool registered = true;
using CoreType = operators::PreparedStellarEquilibriumOperator;
};
template <typename EquationOfState>
concept ImplementedMaterialSurfaceEquationOfState = requires {
{
MaterialSurfaceEquationOfStateBackend<std::remove_cvref_t<EquationOfState>>::registered
} -> std::convertible_to<bool>;
requires MaterialSurfaceEquationOfStateBackend<
std::remove_cvref_t<EquationOfState>>::registered;
typename MaterialSurfaceEquationOfStateBackend<std::remove_cvref_t<EquationOfState>>::CoreType;
};
/*
* Registering an EOS-to-core association is intentionally not enough to
* claim that the material/surface preconditioner can execute it. Every
* implementation listed here must have matching prepared operators and
* prepare(...) overloads below. A future backend should add its pair only
* after those executable pieces exist; this keeps capability queries
* truthful while the current kernels still consume the legacy physical
* core directly.
*/
template <typename EquationOfState, typename PhysicalCore>
struct MaterialSurfaceExecutableRuntime {
static constexpr bool available = false;
};
template <>
struct MaterialSurfaceExecutableRuntime<eos::Polytrope, operators::PreparedStellarEquilibriumOperator> {
static constexpr bool available = true;
};
template <typename EquationOfState, typename PhysicalCore>
concept ExecutableMaterialSurfaceRuntimeFor = requires {
{
MaterialSurfaceExecutableRuntime<
std::remove_cvref_t<EquationOfState>,
std::remove_cvref_t<PhysicalCore>>::available
} -> std::convertible_to<bool>;
requires MaterialSurfaceExecutableRuntime<
std::remove_cvref_t<EquationOfState>,
std::remove_cvref_t<PhysicalCore>>::available;
};
template <typename Descriptor> template <typename Descriptor>
concept ImplementedMaterialSurfaceDescriptor = concept ImplementedMaterialSurfaceDescriptor =
MaterialSurfaceDescriptor<Descriptor> && MaterialSurfaceDescriptor<Descriptor> &&
std::same_as<typename Descriptor::ThermodynamicEquations::EquationOfStateType, eos::Polytrope>; ImplementedMaterialSurfaceEquationOfState<
typename Descriptor::ThermodynamicEquations::EquationOfStateType>;
template <typename Descriptor, typename PhysicalCore>
concept MaterialSurfaceRuntimeFor =
ImplementedMaterialSurfaceDescriptor<Descriptor> && requires {
typename MaterialSurfaceEquationOfStateBackend<
typename std::remove_cvref_t<Descriptor>::ThermodynamicEquations::EquationOfStateType>::CoreType;
requires std::same_as<
std::remove_cvref_t<PhysicalCore>,
typename MaterialSurfaceEquationOfStateBackend<
typename std::remove_cvref_t<Descriptor>::ThermodynamicEquations::EquationOfStateType>::CoreType>;
requires ExecutableMaterialSurfaceRuntimeFor<
typename std::remove_cvref_t<Descriptor>::ThermodynamicEquations::EquationOfStateType,
PhysicalCore>;
};
template <typename Candidate>
concept MaterialSurfacePreconditionerProblem =
equilibrium::DiscretizedStellarEquilibriumProblem<Candidate> && requires {
requires MaterialSurfaceRuntimeFor<
MaterialSurfaceDescriptorFor<std::remove_cvref_t<Candidate>>,
typename std::remove_cvref_t<Candidate>::PhysicalCoreType>;
};
using DensityMassDiagonalCharacteristics = OperatorCharacteristics< using DensityMassDiagonalCharacteristics = OperatorCharacteristics<
OperatorCategory::mass_like, OperatorCategory::mass_like,
@@ -605,7 +685,7 @@ export namespace mean_field::preconditioning {
}; };
template < template <
equilibrium::DiscretizedStellarEquilibriumProblem Problem, MaterialSurfacePreconditionerProblem Problem,
backend::Registered MaterialBackend = backend::Diagonal, backend::Registered MaterialBackend = backend::Diagonal,
backend::Registered SurfaceBackend = backend::Diagonal, backend::Registered SurfaceBackend = backend::Diagonal,
MaterialSurfaceFactorizationPolicy Policy = SurfaceThenMaterialTriangular> MaterialSurfaceFactorizationPolicy Policy = SurfaceThenMaterialTriangular>
@@ -623,7 +703,7 @@ export namespace mean_field::preconditioning {
} }
template < template <
equilibrium::DiscretizedStellarEquilibriumProblem Problem, MaterialSurfacePreconditionerProblem Problem,
backend::Registered MaterialBackend, backend::Registered MaterialBackend,
backend::Registered SurfaceBackend, backend::Registered SurfaceBackend,
MaterialSurfaceFactorizationPolicy Policy, MaterialSurfaceFactorizationPolicy Policy,
@@ -688,7 +768,7 @@ export namespace mean_field::preconditioning {
// direct coupling actions and does not pay for a full Jacobian // direct coupling actions and does not pay for a full Jacobian
// application. // application.
m_fullDirection = 0.0; m_fullDirection = 0.0;
const auto fullDirectionView = m_operation->GetRootManifest().directionView(m_fullDirection); const auto fullDirectionView = m_operation->GetRootManifest().stateView(m_fullDirection);
mfem::Vector fullDensityDirection = fullDirectionView.block(utils::blocks::density_field.mass_term); mfem::Vector fullDensityDirection = fullDirectionView.block(utils::blocks::density_field.mass_term);
mfem::Vector fullSurfaceDirection = mfem::Vector fullSurfaceDirection =
fullDirectionView.block(utils::blocks::surface_deformation_field.parameters_term); fullDirectionView.block(utils::blocks::surface_deformation_field.parameters_term);
@@ -699,10 +779,10 @@ export namespace mean_field::preconditioning {
m_operation->Mult(m_fullDirection, m_fullAction); m_operation->Mult(m_fullDirection, m_fullAction);
const auto fullActionView = m_operation->GetRootManifest().residualView(m_fullAction); const auto fullActionView = m_operation->GetRootManifest().residualView(m_fullAction);
const mfem::Vector fullDensityAction = fullActionView.block(utils::blocks::density_field.mass_term); const auto fullDensityAction = fullActionView.block(utils::blocks::density_field.mass_term);
const mfem::Vector fullSurfaceAction = const auto fullSurfaceAction =
fullActionView.block(utils::blocks::surface_deformation_field.shape_equilibrium_term); fullActionView.block(utils::blocks::surface_deformation_field.shape_equilibrium_term);
const mfem::Vector fullEnthalpyAction = fullActionView.block(utils::blocks::enthalpy_field.specific_term); const auto fullEnthalpyAction = fullActionView.block(utils::blocks::enthalpy_field.specific_term);
densityAction = fullDensityAction; densityAction = fullDensityAction;
surfaceAction = fullSurfaceAction; surfaceAction = fullSurfaceAction;
enthalpyAction = fullEnthalpyAction; enthalpyAction = fullEnthalpyAction;
@@ -1108,7 +1188,8 @@ export namespace mean_field::preconditioning {
std::uint64_t surfaceH1Assemblies{0}; std::uint64_t surfaceH1Assemblies{0};
}; };
template <ImplementedMaterialSurfaceDescriptor Descriptor, MaterialSurfaceFactorizationPolicy Policy> template <MaterialSurfaceDescriptor Descriptor, MaterialSurfaceFactorizationPolicy Policy>
requires MaterialSurfaceRuntimeFor<Descriptor, operators::PreparedStellarEquilibriumOperator>
class PreparedMaterialSurfaceBlock final : public mfem::Solver { class PreparedMaterialSurfaceBlock final : public mfem::Solver {
public: public:
using Block = MaterialSurfaceBlock<Descriptor, backend::Diagonal, backend::Diagonal, Policy>; using Block = MaterialSurfaceBlock<Descriptor, backend::Diagonal, backend::Diagonal, Policy>;
@@ -1562,9 +1643,10 @@ export namespace mean_field::preconditioning {
}; };
template < template <
ImplementedMaterialSurfaceDescriptor Descriptor, MaterialSurfaceDescriptor Descriptor,
MaterialSurfaceFactorizationPolicy Policy, MaterialSurfaceFactorizationPolicy Policy,
backend::ApplicationMode Mode> backend::ApplicationMode Mode>
requires MaterialSurfaceRuntimeFor<Descriptor, operators::PreparedStellarEquilibriumOperator>
class PreparedH1MaterialSurfaceBlock final : public mfem::Solver { class PreparedH1MaterialSurfaceBlock final : public mfem::Solver {
public: public:
using SurfaceBackend = backend::HypreBoomerAMG<Mode>; using SurfaceBackend = backend::HypreBoomerAMG<Mode>;
@@ -2027,8 +2109,9 @@ export namespace mean_field::preconditioning {
}; };
template < template <
ImplementedMaterialSurfaceDescriptor Descriptor, MaterialSurfaceDescriptor Descriptor,
MaterialSurfaceFactorizationPolicy Policy> MaterialSurfaceFactorizationPolicy Policy>
requires MaterialSurfaceRuntimeFor<Descriptor, operators::PreparedStellarEquilibriumOperator>
[[nodiscard]] auto prepare( [[nodiscard]] auto prepare(
const operators::PreparedStellarEquilibriumOperator &operation, const operators::PreparedStellarEquilibriumOperator &operation,
MaterialSurfaceBlock< MaterialSurfaceBlock<
@@ -2042,26 +2125,26 @@ export namespace mean_field::preconditioning {
template < template <
equilibrium::StellarEquilibriumModel Model, equilibrium::StellarEquilibriumModel Model,
equilibrium::StellarDiscretizationType Discretization,
MaterialSurfaceFactorizationPolicy Policy> MaterialSurfaceFactorizationPolicy Policy>
requires MaterialSurfacePreconditionerProblem<
equilibrium::StellarEquilibriumProblem<Model, Discretization>>
[[nodiscard]] auto prepare( [[nodiscard]] auto prepare(
const equilibrium::StellarEquilibriumProblem<Model> &problem, const equilibrium::StellarEquilibriumProblem<Model, Discretization> &problem,
MaterialSurfaceBlock< MaterialSurfaceBlock<
MaterialSurfaceDescriptorFor<equilibrium::StellarEquilibriumProblem<Model>>, MaterialSurfaceDescriptorFor<equilibrium::StellarEquilibriumProblem<Model, Discretization>>,
backend::Diagonal, backend::Diagonal,
backend::Diagonal, backend::Diagonal,
Policy> block Policy> block
) { ) {
if constexpr (equilibrium::StellarEquilibriumProblem<Model>::hasFixedCentralDensity) { return prepare(problem.GetPhysicalOperator(), std::move(block));
return prepare(problem.GetPreparedOperator().GetPhysicalOperator(), std::move(block));
} else {
return prepare(problem.GetPreparedOperator(), std::move(block));
}
} }
template < template <
ImplementedMaterialSurfaceDescriptor Descriptor, MaterialSurfaceDescriptor Descriptor,
MaterialSurfaceFactorizationPolicy Policy, MaterialSurfaceFactorizationPolicy Policy,
backend::ApplicationMode Mode> backend::ApplicationMode Mode>
requires MaterialSurfaceRuntimeFor<Descriptor, operators::PreparedStellarEquilibriumOperator>
[[nodiscard]] auto prepare( [[nodiscard]] auto prepare(
const operators::PreparedStellarEquilibriumOperator &operation, const operators::PreparedStellarEquilibriumOperator &operation,
MaterialSurfaceBlock< MaterialSurfaceBlock<
@@ -2076,21 +2159,20 @@ export namespace mean_field::preconditioning {
template < template <
equilibrium::StellarEquilibriumModel Model, equilibrium::StellarEquilibriumModel Model,
equilibrium::StellarDiscretizationType Discretization,
MaterialSurfaceFactorizationPolicy Policy, MaterialSurfaceFactorizationPolicy Policy,
backend::ApplicationMode Mode> backend::ApplicationMode Mode>
requires MaterialSurfacePreconditionerProblem<
equilibrium::StellarEquilibriumProblem<Model, Discretization>>
[[nodiscard]] auto prepare( [[nodiscard]] auto prepare(
const equilibrium::StellarEquilibriumProblem<Model> &problem, const equilibrium::StellarEquilibriumProblem<Model, Discretization> &problem,
MaterialSurfaceBlock< MaterialSurfaceBlock<
MaterialSurfaceDescriptorFor<equilibrium::StellarEquilibriumProblem<Model>>, MaterialSurfaceDescriptorFor<equilibrium::StellarEquilibriumProblem<Model, Discretization>>,
backend::Diagonal, backend::Diagonal,
backend::HypreBoomerAMG<Mode>, backend::HypreBoomerAMG<Mode>,
Policy, Policy,
SurfaceH1MassStiffness> block SurfaceH1MassStiffness> block
) { ) {
if constexpr (equilibrium::StellarEquilibriumProblem<Model>::hasFixedCentralDensity) { return prepare(problem.GetPhysicalOperator(), std::move(block));
return prepare(problem.GetPreparedOperator().GetPhysicalOperator(), std::move(block));
} else {
return prepare(problem.GetPreparedOperator(), std::move(block));
}
} }
} // namespace mean_field::preconditioning } // namespace mean_field::preconditioning

View File

@@ -24,6 +24,7 @@ export namespace mean_field::preconditioning {
operators::StellarEquilibriumDependencyStamp geometry; operators::StellarEquilibriumDependencyStamp geometry;
const void *equationOfStateIdentity{nullptr}; const void *equationOfStateIdentity{nullptr};
operators::StellarEquilibriumDependencies linearization; operators::StellarEquilibriumDependencies linearization;
std::uint64_t preparedOperatorGeneration{0};
constexpr bool operator==(const StellarPreconditionerLifecycleSnapshot &) const = default; constexpr bool operator==(const StellarPreconditionerLifecycleSnapshot &) const = default;
}; };
@@ -61,7 +62,8 @@ export namespace mean_field::preconditioning {
.discretization = prepared.discretization != current.discretization, .discretization = prepared.discretization != current.discretization,
.geometry = prepared.geometry != current.geometry, .geometry = prepared.geometry != current.geometry,
.equationOfState = prepared.equationOfStateIdentity != current.equationOfStateIdentity, .equationOfState = prepared.equationOfStateIdentity != current.equationOfStateIdentity,
.linearization = prepared.linearization != current.linearization .linearization = prepared.linearization != current.linearization ||
prepared.preparedOperatorGeneration != current.preparedOperatorGeneration
}; };
} }
@@ -95,9 +97,11 @@ export namespace mean_field::preconditioning {
static constexpr bool registered = false; static constexpr bool registered = false;
}; };
template <equilibrium::StellarEquilibriumModel Model> template <
struct StellarEquilibriumProblemTraits<equilibrium::StellarEquilibriumProblem<Model>> { equilibrium::StellarEquilibriumModel Model,
using Problem = equilibrium::StellarEquilibriumProblem<Model>; equilibrium::StellarDiscretizationType Discretization>
struct StellarEquilibriumProblemTraits<equilibrium::StellarEquilibriumProblem<Model, Discretization>> {
using Problem = equilibrium::StellarEquilibriumProblem<Model, Discretization>;
using Form = typename Problem::FormType; using Form = typename Problem::FormType;
using JacobianForm = typename Problem::JacobianFormType; using JacobianForm = typename Problem::JacobianFormType;
using Manifest = typename Problem::ManifestType; using Manifest = typename Problem::ManifestType;
@@ -130,8 +134,9 @@ export namespace mean_field::preconditioning {
.discretization = dependencies.discretization, .discretization = dependencies.discretization,
.geometry = problem.GetGeometryDependency(), .geometry = problem.GetGeometryDependency(),
.equationOfStateIdentity = .equationOfStateIdentity =
std::addressof(problem.GetStellarModel().template specification<eos::Polytrope>()), std::addressof(problem.GetStellarModel().equationOfState()),
.linearization = dependencies .linearization = dependencies,
.preparedOperatorGeneration = problem.GetPreparationGeneration()
}; };
} }
}; };
@@ -139,6 +144,243 @@ export namespace mean_field::preconditioning {
template <typename Candidate> template <typename Candidate>
concept StellarPreconditionerProblem = StellarEquilibriumProblemTraits<std::remove_cvref_t<Candidate>>::registered; concept StellarPreconditionerProblem = StellarEquilibriumProblemTraits<std::remove_cvref_t<Candidate>>::registered;
namespace detail {
template <typename Block>
struct IsGeneratedStellarValueBlock : std::false_type { };
template <typename Generated>
struct IsGeneratedStellarValueBlock<utils::blocks::generated_value_block<Generated>>
: std::true_type { };
template <typename Block>
struct IsGeneratedStellarResidualBlock : std::false_type { };
template <typename Generated>
struct IsGeneratedStellarResidualBlock<utils::blocks::generated_residual_block<Generated>>
: std::true_type { };
template <typename Coupling>
inline constexpr bool isPurePhysicalStellarCoupling =
!IsGeneratedStellarValueBlock<
std::remove_cvref_t<typename Coupling::Value>>::value &&
!IsGeneratedStellarResidualBlock<
std::remove_cvref_t<typename Coupling::Residual>>::value;
/* Pure structure contributions owned by a trusted backend are exact
* (core, specification, coupling) capabilities. Future cores and new
* edges start with no privilege: changing a built-in declaration must
* be accompanied by an explicit preconditioner decision. Generated-
* border terms remain the responsibility of specification-border
* machinery. */
template <typename PhysicalCore, typename Specification>
struct StellarStructureBackendHandledCouplings {
using Type = utils::blocks::type_list<>;
};
template <>
struct StellarStructureBackendHandledCouplings<
operators::PreparedStellarEquilibriumOperator,
models::FixedTotalMass> {
using Type = utils::blocks::type_list<
operators::StellarEquilibriumJacobianCoupling<
utils::blocks::enthalpy::specific::residual,
utils::blocks::density::mass::value>,
operators::StellarEquilibriumJacobianCoupling<
utils::blocks::enthalpy::specific::residual,
utils::blocks::surface_deformation::parameters::value>>;
};
template <>
struct StellarStructureBackendHandledCouplings<
operators::PreparedStellarEquilibriumOperator,
models::FixedAngularMomentum> {
using Type = utils::blocks::type_list<
operators::StellarEquilibriumJacobianCoupling<
utils::blocks::surface_deformation::shape_equilibrium::residual,
utils::blocks::density::mass::value>,
operators::StellarEquilibriumJacobianCoupling<
utils::blocks::surface_deformation::shape_equilibrium::residual,
utils::blocks::surface_deformation::parameters::value>,
operators::StellarEquilibriumJacobianCoupling<
utils::blocks::enthalpy::specific::residual,
utils::blocks::density::mass::value>,
operators::StellarEquilibriumJacobianCoupling<
utils::blocks::enthalpy::specific::residual,
utils::blocks::surface_deformation::parameters::value>>;
};
template <>
struct StellarStructureBackendHandledCouplings<
operators::PreparedStellarEquilibriumOperator,
models::FixedCentralDensity> {
using Type = utils::blocks::type_list<
operators::StellarEquilibriumJacobianCoupling<
utils::blocks::enthalpy::specific::residual,
utils::blocks::enthalpy::specific::value>>;
};
template <
typename Coupling,
typename Model,
typename PhysicalCore,
typename ModelSpecifications>
struct EveryCouplingContributionHandled;
template <
typename Coupling,
model::StellarModelType Model,
typename PhysicalCore,
models::ModelSpecification... Specifications>
struct EveryCouplingContributionHandled<
Coupling,
Model,
PhysicalCore,
models::detail::SpecificationSetStorage<Specifications...>> final {
private:
template <typename Specification>
static constexpr bool handled =
!utils::blocks::contains_type_v<
Coupling,
typename operators::StellarEquilibriumSpecificationCompilation<
Specification>::JacobianCouplings> ||
(operators::stellarEquilibriumBackendRuntimeAuthorized<
Specification,
Model> &&
utils::blocks::contains_type_v<
Coupling,
typename StellarStructureBackendHandledCouplings<
PhysicalCore,
Specification>::Type>) ||
operators::stellarEquilibriumSpecificationCouplingIsStructuralZero<
Specification,
Model,
Coupling>;
public:
static constexpr bool value =
(handled<Specifications> && ...);
};
template <
typename Remaining,
typename Model,
typename PhysicalCore,
typename ModelSpecifications,
typename Unsupported>
struct CollectUnsupportedStellarStructureCouplings;
template <
typename Model,
typename PhysicalCore,
typename ModelSpecifications,
typename Unsupported>
struct CollectUnsupportedStellarStructureCouplings<
utils::blocks::type_list<>,
Model,
PhysicalCore,
ModelSpecifications,
Unsupported> {
using Type = Unsupported;
};
template <
typename Head,
typename... Tail,
typename Model,
typename PhysicalCore,
typename ModelSpecifications,
typename... Unsupported>
struct CollectUnsupportedStellarStructureCouplings<
utils::blocks::type_list<Head, Tail...>,
Model,
PhysicalCore,
ModelSpecifications,
utils::blocks::type_list<Unsupported...>> {
private:
static constexpr bool supported =
!isPurePhysicalStellarCoupling<Head> ||
EveryCouplingContributionHandled<
Head,
Model,
PhysicalCore,
ModelSpecifications>::value;
using Next = std::conditional_t<
supported,
utils::blocks::type_list<Unsupported...>,
utils::blocks::type_list<Unsupported..., Head>>;
public:
using Type = typename CollectUnsupportedStellarStructureCouplings<
utils::blocks::type_list<Tail...>,
Model,
PhysicalCore,
ModelSpecifications,
Next>::Type;
};
template <typename Candidate, typename = void>
struct DefaultStellarStructurePhysicalTopologyAudit {
using ContributionCouplings = utils::blocks::type_list<>;
using UnsupportedCouplings = utils::blocks::type_list<>;
static constexpr bool supported = false;
};
template <model::StellarModelType Model>
requires(
operators::StellarEquilibriumSystemCompilable<
std::remove_cvref_t<Model>> &&
operators::hasStellarEquilibriumCoreRuntime<
std::remove_cvref_t<Model>>)
struct DefaultStellarStructurePhysicalTopologyAudit<
Model,
std::void_t<
typename operators::CompiledStellarEquilibriumSystem<
std::remove_cvref_t<Model>>::ContributionJacobianCouplings,
operators::StellarEquilibriumPhysicalCoreType<
std::remove_cvref_t<Model>>>> {
private:
using Compilation = operators::CompiledStellarEquilibriumSystem<
std::remove_cvref_t<Model>>;
using PhysicalCore = operators::StellarEquilibriumPhysicalCoreType<
std::remove_cvref_t<Model>>;
public:
using ContributionCouplings =
typename Compilation::ContributionJacobianCouplings;
using UnsupportedCouplings =
typename CollectUnsupportedStellarStructureCouplings<
ContributionCouplings,
std::remove_cvref_t<Model>,
PhysicalCore,
typename std::remove_cvref_t<Model>::SpecificationTypes,
utils::blocks::type_list<>>::Type;
static constexpr bool supported = UnsupportedCouplings::size == 0;
};
} // namespace detail
/* A generated-border edge is owned by specification-border machinery and
* is deliberately ignored here. Every pure physical edge contributed by a
* model must be owned by the selected numerical structure backend, or
* every non-backend provider of that edge must prove StructuralZero. In
* particular, merely overlapping an existing base-Jacobian edge is not
* sufficient: a custom nonzero coefficient on that edge would otherwise
* disappear silently from the default preconditioner. This audit is
* detection-safe and therefore suitable for constraining factories. */
template <typename Candidate>
struct DefaultStellarStructurePhysicalTopologySupport
: detail::DefaultStellarStructurePhysicalTopologyAudit<
std::remove_cvref_t<Candidate>> { };
template <typename Candidate>
inline constexpr bool defaultStellarStructurePhysicalTopologySupported =
DefaultStellarStructurePhysicalTopologySupport<
std::remove_cvref_t<Candidate>>::supported;
template <typename Candidate>
concept DefaultStellarStructurePhysicalTopologySupportedFor =
defaultStellarStructurePhysicalTopologySupported<Candidate>;
namespace backend { namespace backend {
template <typename Component, typename Problem, typename Backend = typename Component::BackendType> template <typename Component, typename Problem, typename Backend = typename Component::BackendType>
class PreparedComponent; class PreparedComponent;
@@ -174,56 +416,17 @@ export namespace mean_field::preconditioning {
} // namespace backend } // namespace backend
namespace detail { namespace detail {
using DensityIdentity =
IdentityBlock<utils::blocks::density::mass::value, utils::blocks::density::mass::residual>;
using SurfaceIdentity = IdentityBlock<
utils::blocks::surface_deformation::parameters::value,
utils::blocks::surface_deformation::shape_equilibrium::residual>;
using GravityGradientIdentity =
IdentityBlock<utils::blocks::gravity::gradient::value, utils::blocks::gravity::gradient::residual>;
using GravityPotentialIdentity =
IdentityBlock<utils::blocks::gravity::poisson::value, utils::blocks::gravity::poisson::residual>;
using EnthalpyIdentity =
IdentityBlock<utils::blocks::enthalpy::specific::value, utils::blocks::enthalpy::specific::residual>;
using FixedMassIdentity = IdentityBlock<
utils::blocks::fixed_total_mass::mass_normalization::value,
utils::blocks::fixed_total_mass::mass_normalization::residual>;
using FixedCentralDensityIdentity = IdentityBlock<
utils::blocks::fixed_central_density::central_value::value,
utils::blocks::fixed_central_density::central_value::residual>;
template <typename Form> struct IdentityPlanForForm; template <typename Form> struct IdentityPlanForForm;
template <> struct IdentityPlanForForm<utils::blocks::surface_deformed_stellar_equilibrium_form> { template <typename... Values, typename... Residuals>
using Type = PreconditionerPlan< struct IdentityPlanForForm<utils::blocks::block_form<
DensityIdentity, utils::blocks::type_list<Values...>,
SurfaceIdentity, utils::blocks::type_list<Residuals...>>> {
GravityGradientIdentity, static_assert(sizeof...(Values) == sizeof...(Residuals));
GravityPotentialIdentity, using Type = PreconditionerPlan<IdentityBlock<Values, Residuals>...>;
EnthalpyIdentity,
FixedMassIdentity>;
[[nodiscard]] static constexpr Type Make() { [[nodiscard]] static constexpr Type Make() {
return Type{DensityIdentity{}, SurfaceIdentity{}, GravityGradientIdentity{}, return Type{IdentityBlock<Values, Residuals>{}...};
GravityPotentialIdentity{}, EnthalpyIdentity{}, FixedMassIdentity{}};
}
};
template <> struct IdentityPlanForForm<utils::blocks::central_density_bordered_stellar_equilibrium_form> {
using Type = PreconditionerPlan<
DensityIdentity,
SurfaceIdentity,
GravityGradientIdentity,
GravityPotentialIdentity,
EnthalpyIdentity,
FixedMassIdentity,
FixedCentralDensityIdentity>;
[[nodiscard]] static constexpr Type Make() {
return Type{
DensityIdentity{}, SurfaceIdentity{}, GravityGradientIdentity{}, GravityPotentialIdentity{},
EnthalpyIdentity{}, FixedMassIdentity{}, FixedCentralDensityIdentity{}
};
} }
}; };

View File

@@ -14,6 +14,7 @@ export module mean_field:preconditioning.stellar_structure;
export import :preconditioning.gravity_field; export import :preconditioning.gravity_field;
export import :preconditioning.material_surface; export import :preconditioning.material_surface;
export import :preconditioning.stellar_equilibrium;
export namespace mean_field::preconditioning { export namespace mean_field::preconditioning {
struct IndependentStellarSubsystems final { }; struct IndependentStellarSubsystems final { };
@@ -626,28 +627,88 @@ export namespace mean_field::preconditioning {
} }
}; };
/*
* Material/surface execution and stellar-structure execution are separate
* capabilities. The latter also owns the physical cross-Jacobian and
* gravity-context wiring, which currently target the legacy prepared core.
* Add future cores here only together with matching cross-coupling and
* preparation implementations.
*/
template <typename PhysicalCore>
struct StellarStructureExecutableRuntime {
static constexpr bool available = false;
};
template <>
struct StellarStructureExecutableRuntime<operators::PreparedStellarEquilibriumOperator> {
static constexpr bool available = true;
};
template <typename PhysicalCore>
concept ExecutableStellarStructureRuntimeFor = requires {
{
StellarStructureExecutableRuntime<std::remove_cvref_t<PhysicalCore>>::available
} -> std::convertible_to<bool>;
requires StellarStructureExecutableRuntime<std::remove_cvref_t<PhysicalCore>>::available;
};
template <typename Descriptor, typename PhysicalCore>
concept StellarStructureRuntimeFor =
MaterialSurfaceRuntimeFor<Descriptor, PhysicalCore> &&
ExecutableStellarStructureRuntimeFor<PhysicalCore>;
template <typename Candidate>
concept StellarStructurePreconditionerProblem =
equilibrium::DiscretizedStellarEquilibriumProblem<Candidate> &&
DefaultStellarStructurePhysicalTopologySupportedFor<
typename std::remove_cvref_t<Candidate>::ModelType> &&
requires {
requires StellarStructureRuntimeFor<
MaterialSurfaceDescriptorFor<std::remove_cvref_t<Candidate>>,
typename std::remove_cvref_t<Candidate>::PhysicalCoreType>;
};
namespace detail { namespace detail {
template <equilibrium::StellarEquilibriumModel Model> template <StellarStructurePreconditionerProblem Problem>
[[nodiscard]] const operators::PreparedStellarEquilibriumOperator & [[nodiscard]] const auto &physicalOperator(const Problem &problem) {
physicalOperator(const equilibrium::StellarEquilibriumProblem<Model> &problem) { return problem.GetPhysicalOperator();
if constexpr (equilibrium::StellarEquilibriumProblem<Model>::hasFixedCentralDensity) {
return problem.GetPreparedOperator().GetPhysicalOperator();
} else {
return problem.GetPreparedOperator();
}
} }
} // namespace detail } // namespace detail
template <typename Problem, typename MaterialComponent, typename GravityComponent>
concept StellarStructurePreparableFor =
StellarStructurePreconditionerProblem<std::remove_cvref_t<Problem>> &&
PreconditionerComponent<std::remove_cvref_t<MaterialComponent>> &&
PreconditionerComponent<std::remove_cvref_t<GravityComponent>> &&
requires(
const std::remove_cvref_t<Problem> &problem,
std::remove_cvref_t<MaterialComponent> materialComponent,
std::remove_cvref_t<GravityComponent> gravityComponent
) {
preconditioning::prepare(problem, std::move(materialComponent));
preconditioning::prepare(
problem.GetPhysicalOperator().GetHydrostaticOperator().GetFEM(),
problem.GetPhysicalOperator().GetGravityContext().GetGeometryContext(),
std::move(gravityComponent)
);
StellarStructureCrossJacobianOperator{problem.GetPhysicalOperator()};
};
template < template <
equilibrium::StellarEquilibriumModel Model, equilibrium::StellarEquilibriumModel Model,
equilibrium::StellarDiscretizationType Discretization,
typename MaterialComponent, typename MaterialComponent,
backend::Registered GravityMassBackend, backend::Registered GravityMassBackend,
backend::ApplicationMode Mode, backend::ApplicationMode Mode,
GravityFactorizationPolicy GravityPolicy, GravityFactorizationPolicy GravityPolicy,
StellarStructureFactorizationPolicy StructurePolicy> StellarStructureFactorizationPolicy StructurePolicy>
requires StellarStructurePreparableFor<
equilibrium::StellarEquilibriumProblem<Model, Discretization>,
MaterialComponent,
GravityFieldBlock<GravityMassBackend, backend::HypreBoomerAMG<Mode>, GravityPolicy>>
class PreparedStellarStructureBlock final : public mfem::Solver { class PreparedStellarStructureBlock final : public mfem::Solver {
private: private:
using Problem = equilibrium::StellarEquilibriumProblem<Model>; using Problem = equilibrium::StellarEquilibriumProblem<Model, Discretization>;
using GravityComponent = GravityFieldBlock<GravityMassBackend, backend::HypreBoomerAMG<Mode>, GravityPolicy>; using GravityComponent = GravityFieldBlock<GravityMassBackend, backend::HypreBoomerAMG<Mode>, GravityPolicy>;
using Structure = StellarStructureBlock< using Structure = StellarStructureBlock<
MaterialComponent, MaterialComponent,
@@ -769,7 +830,7 @@ export namespace mean_field::preconditioning {
}; };
template < template <
equilibrium::DiscretizedStellarEquilibriumProblem Problem, StellarStructurePreconditionerProblem Problem,
typename MaterialComponent, typename MaterialComponent,
backend::Registered GravityMassBackend, backend::Registered GravityMassBackend,
backend::ApplicationMode Mode, backend::ApplicationMode Mode,
@@ -792,7 +853,7 @@ export namespace mean_field::preconditioning {
}; };
} }
template <equilibrium::DiscretizedStellarEquilibriumProblem Problem> template <StellarStructurePreconditionerProblem Problem>
[[nodiscard]] constexpr auto stellarStructureBlock(const Problem &problem) { [[nodiscard]] constexpr auto stellarStructureBlock(const Problem &problem) {
using FixedAMG = backend::HypreBoomerAMG<backend::FixedCycles>; using FixedAMG = backend::HypreBoomerAMG<backend::FixedCycles>;
auto material = materialSurfaceBlock(problem); auto material = materialSurfaceBlock(problem);
@@ -805,25 +866,30 @@ export namespace mean_field::preconditioning {
template < template <
equilibrium::StellarEquilibriumModel Model, equilibrium::StellarEquilibriumModel Model,
equilibrium::StellarDiscretizationType Discretization,
typename MaterialComponent, typename MaterialComponent,
backend::Registered GravityMassBackend, backend::Registered GravityMassBackend,
backend::ApplicationMode Mode, backend::ApplicationMode Mode,
GravityFactorizationPolicy GravityPolicy, GravityFactorizationPolicy GravityPolicy,
StellarStructureFactorizationPolicy StructurePolicy> StellarStructureFactorizationPolicy StructurePolicy>
requires StellarStructurePreparableFor<
equilibrium::StellarEquilibriumProblem<Model, Discretization>,
MaterialComponent,
GravityFieldBlock<GravityMassBackend, backend::HypreBoomerAMG<Mode>, GravityPolicy>>
[[nodiscard]] auto prepare( [[nodiscard]] auto prepare(
const equilibrium::StellarEquilibriumProblem<Model> &problem, const equilibrium::StellarEquilibriumProblem<Model, Discretization> &problem,
StellarStructureBlock< StellarStructureBlock<
MaterialComponent, MaterialComponent,
GravityFieldBlock< GravityFieldBlock<
GravityMassBackend, GravityMassBackend,
backend::HypreBoomerAMG<Mode>, backend::HypreBoomerAMG<Mode>,
GravityPolicy>, GravityPolicy>,
typename equilibrium::StellarEquilibriumProblem<Model>::FormType, typename equilibrium::StellarEquilibriumProblem<Model, Discretization>::FormType,
typename equilibrium::StellarEquilibriumProblem<Model>::JacobianFormType, typename equilibrium::StellarEquilibriumProblem<Model, Discretization>::JacobianFormType,
StructurePolicy> structure StructurePolicy> structure
) { ) {
return PreparedStellarStructureBlock< return PreparedStellarStructureBlock<
Model, MaterialComponent, GravityMassBackend, Mode, GravityPolicy, StructurePolicy>{ Model, Discretization, MaterialComponent, GravityMassBackend, Mode, GravityPolicy, StructurePolicy>{
problem, std::move(structure) problem, std::move(structure)
}; };
} }

View File

@@ -1,5 +1,7 @@
module; module;
#include <cmath>
#include <cstddef>
#include <concepts> #include <concepts>
#include <stdexcept> #include <stdexcept>
#include <type_traits> #include <type_traits>
@@ -25,6 +27,65 @@ export namespace mean_field::seed {
mfem::Vector values; mfem::Vector values;
}; };
/*
* The public radial-projection extension boundary deliberately speaks in
* physical state names. A specification author supplies one small rule
* and opts in with
*
* using RadialProjection = seed::projection::Use<MyProjectionPhysics>;
*
* There is no registry ordinal and no model-combination specialization.
*/
struct RadialProjectionScales final {
dimensions::MassValue targetMass;
dimensions::LengthValue stellarRadius;
double bernoulliConstant;
double sphericalMomentOfInertia;
const mfem::Array<int> *surfaceCarrierRows;
};
struct RadialProjectionState final {
mfem::Vector density;
mfem::Vector surfaceShape;
mfem::Vector gravityGradient;
mfem::Vector gravityPotential;
mfem::Vector specificEnthalpy;
};
namespace projection {
template <typename Physics> struct Use final {
using PhysicsType = Physics;
};
/* An explicit opt-in for a specification that leaves a radial seed unchanged. */
struct NoStateChange {
static constexpr bool registered = true;
static constexpr bool providesRadialMass = false;
template <typename Model>
static constexpr bool supports = true;
template <typename Specification, typename Model>
static void validate(
const Specification &,
const Model &,
const RadialProfile &,
const StellarEquilibriumProjectionOptions &
) noexcept {
}
template <typename Specification, typename Model>
static void initialize(
const Specification &,
const Model &,
const RadialProjectionScales &,
RadialProjectionState &,
mfem::Vector
) noexcept {
}
};
} // namespace projection
namespace detail { namespace detail {
struct ProjectedRadialFields final { struct ProjectedRadialFields final {
mfem::Vector density; mfem::Vector density;
@@ -32,13 +93,13 @@ export namespace mean_field::seed {
mfem::Vector gravityPotential; mfem::Vector gravityPotential;
mfem::Vector specificEnthalpy; mfem::Vector specificEnthalpy;
double bernoulliConstant; double bernoulliConstant;
double sphericalMomentOfInertia;
}; };
[[nodiscard]] ProjectedRadialFields projectRadialFields( [[nodiscard]] ProjectedRadialFields projectRadialFields(
const equilibrium::StellarDiscretization &discretization, fem::FEM &finiteElementModel,
const RadialProfile &profile, const RadialProfile &profile,
dimensions::MassValue targetMass, dimensions::MassValue targetMass,
dimensions::PressureValue targetSurfacePressure,
const StellarEquilibriumProjectionOptions &options const StellarEquilibriumProjectionOptions &options
); );
@@ -52,18 +113,402 @@ export namespace mean_field::seed {
} }
destination = source; destination = source;
} }
struct UnavailableRadialProjectionPhysics final {
static constexpr bool registered = false;
static constexpr bool providesRadialMass = false;
template <typename Model>
static constexpr bool supports = false;
};
struct FixedTotalMassRadialProjectionPhysics final {
static constexpr bool registered = true;
static constexpr bool providesRadialMass = true;
template <typename Model>
static constexpr bool supports = true;
[[nodiscard]] static dimensions::MassValue targetMass(const models::FixedTotalMass &specification) {
return specification.targetMass();
}
template <typename Model>
static void validate(
const models::FixedTotalMass &,
const Model &,
const RadialProfile &,
const StellarEquilibriumProjectionOptions &
) noexcept {
}
template <typename Model>
static void initialize(
const models::FixedTotalMass &,
const Model &,
const RadialProjectionScales &scales,
RadialProjectionState &,
mfem::Vector coordinate
) {
if (coordinate.Size() != 1) {
throw std::invalid_argument(
"A fixed-total-mass radial projection requires exactly one generated multiplier."
);
}
coordinate(0) = scales.bernoulliConstant;
}
};
struct IsobaricRadialProjectionPhysics final {
static constexpr bool registered = true;
static constexpr bool providesRadialMass = false;
template <typename Model>
static constexpr bool supports = requires(
const Model &model,
const surface::Isobaric &condition
) {
{
eos::evaluate<dimensions::quantity::SpecificEnthalpy>(
model.equationOfState(), condition.targetPressure()
)
} -> std::same_as<dimensions::SpecificEnthalpyValue>;
};
template <typename Model>
static void validate(
const surface::Isobaric &condition,
const Model &,
const RadialProfile &,
const StellarEquilibriumProjectionOptions &
) {
if (condition.targetPressure().value() != 0.0) {
throw std::invalid_argument("A Lane-Emden radial seed requires a zero-pressure isobaric surface.");
}
}
template <typename Model>
static void initialize(
const surface::Isobaric &condition,
const Model &model,
const RadialProjectionScales &scales,
RadialProjectionState &state,
mfem::Vector coordinate
) {
if (coordinate.Size() != 0) {
throw std::logic_error("An isobaric surface must not generate a radial-seed coordinate.");
}
if (scales.surfaceCarrierRows == nullptr) {
throw std::logic_error("An isobaric radial projection requires compiled surface-carrier rows.");
}
const dimensions::SpecificEnthalpyValue requiredSurfaceEnthalpy =
eos::evaluate<dimensions::quantity::SpecificEnthalpy>(
model.equationOfState(), condition.targetPressure()
);
for (const int surfaceRow : *scales.surfaceCarrierRows) {
state.specificEnthalpy(surfaceRow) = requiredSurfaceEnthalpy.value();
}
}
};
struct FixedCentralDensityRadialProjectionPhysics final {
static constexpr bool registered = true;
static constexpr bool providesRadialMass = false;
template <typename Model>
static constexpr bool supports = true;
template <typename Model>
static void validate(
const models::FixedCentralDensity &,
const Model &,
const RadialProfile &,
const StellarEquilibriumProjectionOptions &
) noexcept {
}
template <typename Model>
static void initialize(
const models::FixedCentralDensity &,
const Model &,
const RadialProjectionScales &,
RadialProjectionState &,
mfem::Vector coordinate
) {
if (coordinate.Size() != 1) {
throw std::invalid_argument(
"A fixed-central-density radial projection requires exactly one generated phase coordinate."
);
}
coordinate(0) = 0.0;
}
};
struct FixedAngularMomentumRadialProjectionPhysics final {
static constexpr bool registered = true;
static constexpr bool providesRadialMass = false;
template <typename Model>
static constexpr bool supports = true;
template <typename Model>
static void validate(
const models::FixedAngularMomentum &,
const Model &,
const RadialProfile &,
const StellarEquilibriumProjectionOptions &
) noexcept {
}
template <typename Model>
static void initialize(
const models::FixedAngularMomentum &constraint,
const Model &,
const RadialProjectionScales &scales,
RadialProjectionState &,
mfem::Vector coordinate
) {
if (coordinate.Size() != 1) {
throw std::logic_error(
"A fixed-angular-momentum radial projection requires one angular-velocity coordinate."
);
}
double centerSquared = 0.0;
double centerAlongAxis = 0.0;
for (std::size_t component = 0; component < constraint.center().size(); ++component) {
centerSquared += constraint.center()[component] * constraint.center()[component];
centerAlongAxis += constraint.center()[component] * constraint.axis()[component];
}
const double parallelAxisCorrection =
scales.targetMass.value() * (centerSquared - centerAlongAxis * centerAlongAxis);
const double momentOfInertia = scales.sphericalMomentOfInertia + parallelAxisCorrection;
if (!std::isfinite(momentOfInertia) || momentOfInertia <= 0.0) {
throw std::runtime_error("The radial seed has no finite, positive axial moment of inertia.");
}
coordinate(0) = constraint.targetAngularMomentum().value() / momentOfInertia;
}
};
template <typename Specification> struct BuiltinRadialProjectionPhysics {
using Type = UnavailableRadialProjectionPhysics;
};
template <> struct BuiltinRadialProjectionPhysics<eos::Polytrope> {
using Type = projection::NoStateChange;
};
template <> struct BuiltinRadialProjectionPhysics<surface::Isobaric> {
using Type = IsobaricRadialProjectionPhysics;
};
template <> struct BuiltinRadialProjectionPhysics<models::FixedTotalMass> {
using Type = FixedTotalMassRadialProjectionPhysics;
};
template <> struct BuiltinRadialProjectionPhysics<models::FixedCentralDensity> {
using Type = FixedCentralDensityRadialProjectionPhysics;
};
template <> struct BuiltinRadialProjectionPhysics<models::FixedAngularMomentum> {
using Type = FixedAngularMomentumRadialProjectionPhysics;
};
template <typename Candidate> struct UnwrapRadialProjectionPhysics {
using Type = UnavailableRadialProjectionPhysics;
static constexpr bool valid = false;
};
template <typename Physics> struct UnwrapRadialProjectionPhysics<projection::Use<Physics>> {
using Type = Physics;
static constexpr bool valid = true;
};
template <typename Specification, typename = void> struct SelectRadialProjectionPhysics {
using Type = typename BuiltinRadialProjectionPhysics<Specification>::Type;
};
template <typename Specification>
struct SelectRadialProjectionPhysics<Specification, std::void_t<typename Specification::RadialProjection>> {
private:
using Wrapped = UnwrapRadialProjectionPhysics<typename Specification::RadialProjection>;
public:
using Type = std::conditional_t<Wrapped::valid, typename Wrapped::Type, UnavailableRadialProjectionPhysics>;
};
template <typename Physics> [[nodiscard]] consteval bool radialProjectionPhysicsRegistered() {
if constexpr (requires {
{ Physics::registered } -> std::convertible_to<bool>;
}) {
return static_cast<bool>(Physics::registered);
} else {
return false;
}
}
template <typename Physics> [[nodiscard]] consteval bool radialProjectionPhysicsProvidesMass() {
if constexpr (requires {
{ Physics::providesRadialMass } -> std::convertible_to<bool>;
}) {
return static_cast<bool>(Physics::providesRadialMass);
} else {
return false;
}
}
template <typename Specification, typename Model>
[[nodiscard]] consteval bool radialProjectionPhysicsIsComplete() {
using Physics = typename SelectRadialProjectionPhysics<Specification>::Type;
if constexpr (!radialProjectionPhysicsRegistered<Physics>()) {
return false;
} else if constexpr (!requires {
{ Physics::template supports<Model> } -> std::convertible_to<bool>;
}) {
return false;
} else if constexpr (!static_cast<bool>(Physics::template supports<Model>)) {
return false;
} else if constexpr (!requires(
const Specification &specification,
const Model &model,
const RadialProfile &profile,
const StellarEquilibriumProjectionOptions &options,
const RadialProjectionScales &scales,
RadialProjectionState &state,
mfem::Vector coordinate
) {
Physics::validate(specification, model, profile, options);
Physics::initialize(specification, model, scales, state, coordinate);
}) {
return false;
} else if constexpr (radialProjectionPhysicsProvidesMass<Physics>()) {
return requires(const Specification &specification) {
{ Physics::targetMass(specification) } -> std::same_as<dimensions::MassValue>;
};
} else {
return true;
}
}
template <models::ModelSpecification Specification, models::GeneratedStateKind Kind>
struct RadialProjectionCoordinateTerm;
template <models::ModelSpecification Specification>
struct RadialProjectionCoordinateTerm<Specification, models::GeneratedStateKind::multiplier> final {
using value = utils::blocks::generated_value_block<models::MultiplierFor<Specification>>;
};
template <models::ModelSpecification Specification>
struct RadialProjectionCoordinateTerm<Specification, models::GeneratedStateKind::physical_coordinate> final {
using value = utils::blocks::generated_value_block<models::PhysicalCoordinateFor<Specification>>;
};
template <models::ModelSpecification Specification>
struct RadialProjectionCoordinateTerm<Specification, models::GeneratedStateKind::solver_border> final {
using value = utils::blocks::generated_value_block<models::BorderFor<Specification>>;
};
template <typename Model, typename SpecificationSet> struct CompileRadialProjection;
template <model::StellarModelType Model, models::ModelSpecification... Specifications>
struct CompileRadialProjection<Model, models::detail::SpecificationSetStorage<Specifications...>> {
using ModelType = std::remove_cvref_t<Model>;
static constexpr std::size_t radialMassProviderCount =
(std::size_t{0} + ... +
(radialProjectionPhysicsProvidesMass<
typename SelectRadialProjectionPhysics<Specifications>::Type>()
? std::size_t{1}
: std::size_t{0}));
static constexpr bool complete = radialMassProviderCount == 1 &&
(radialProjectionPhysicsIsComplete<Specifications, ModelType>() && ...);
[[nodiscard]] static dimensions::MassValue targetMass(const ModelType &model) requires complete {
dimensions::MassValue result{0.0};
([&] {
using Physics = typename SelectRadialProjectionPhysics<Specifications>::Type;
if constexpr (radialProjectionPhysicsProvidesMass<Physics>()) {
result = Physics::targetMass(model.template specification<Specifications>());
}
}(), ...);
return result;
}
static void validate(
const ModelType &model,
const RadialProfile &profile,
const StellarEquilibriumProjectionOptions &options
) requires complete {
([&] {
using Physics = typename SelectRadialProjectionPhysics<Specifications>::Type;
Physics::validate(model.template specification<Specifications>(), model, profile, options);
}(), ...);
}
template <typename StateView>
static void initialize(
const ModelType &model,
const RadialProjectionScales &scales,
RadialProjectionState &state,
const StateView &stateView
) requires complete {
([&] {
using Contribution = models::SpecificationContribution<Specifications>;
using Physics = typename SelectRadialProjectionPhysics<Specifications>::Type;
if constexpr (Contribution::generatedValueArity == 0) {
Physics::initialize(
model.template specification<Specifications>(), model, scales, state, mfem::Vector{}
);
} else {
static_assert(
Contribution::generatedValueArity == 1,
"Radial projection currently requires each specification contribution to generate at "
"most one scalar coordinate."
);
using Term = RadialProjectionCoordinateTerm<Specifications, Contribution::generatedStateKind>;
Physics::initialize(
model.template specification<Specifications>(), model, scales, state,
stateView.block(Term{})
);
}
}(), ...);
}
};
template <typename Candidate, bool = model::StellarModelType<Candidate>>
struct RadialProjectionCompilationAudit {
static constexpr bool complete = false;
};
template <typename Candidate>
struct RadialProjectionCompilationAudit<Candidate, true>
: CompileRadialProjection<Candidate, typename Candidate::SpecificationTypes> { };
} // namespace detail } // namespace detail
template <equilibrium::StellarEquilibriumModel Model> template <typename Candidate>
inline constexpr bool radialProjectionIsCompilable =
detail::RadialProjectionCompilationAudit<std::remove_cvref_t<Candidate>>::complete;
template <typename Candidate>
concept RadialProfileProjectableModel =
model::StellarModelType<Candidate> && radialProjectionIsCompilable<std::remove_cvref_t<Candidate>>;
template <equilibrium::StellarEquilibriumModel Model, equilibrium::StellarDiscretizationType Discretization>
requires RadialProfileProjectableModel<Model>
[[nodiscard]] ProjectedEquilibriumState<Model> projectRadialProfile( [[nodiscard]] ProjectedEquilibriumState<Model> projectRadialProfile(
const equilibrium::StellarEquilibriumProblem<Model> &problem, const equilibrium::StellarEquilibriumProblem<Model, Discretization> &problem,
const RadialProfile &profile, const RadialProfile &profile,
const StellarEquilibriumProjectionOptions &options = {} const StellarEquilibriumProjectionOptions &options = {}
) { ) {
using Projection = detail::CompileRadialProjection<
std::remove_cvref_t<Model>,
typename std::remove_cvref_t<Model>::SpecificationTypes>;
const auto &stellarModel = problem.GetStellarModel();
Projection::validate(stellarModel, profile, options);
const dimensions::MassValue targetMass = Projection::targetMass(stellarModel);
const detail::ProjectedRadialFields fields = detail::projectRadialFields( const detail::ProjectedRadialFields fields = detail::projectRadialFields(
problem.GetDiscretization(), profile, problem.GetDiscretization().finiteElementModel(), profile, targetMass, options
problem.GetStellarModel().template specification<models::FixedTotalMass>().targetMass(),
problem.GetStellarModel().template specification<surface::Isobaric>().targetPressure(), options
); );
mfem::Vector values(problem.StateSize()); mfem::Vector values(problem.StateSize());
@@ -89,45 +534,40 @@ export namespace mean_field::seed {
); );
/* /*
* Projection of a continuous spherical profile onto a faceted * Each specification now initializes only its inferred contribution.
* reference mesh generally leaves a small trace error on the physical * In particular, the surface rule imposes the exact carrier trace and
* surface. The pressure condition replaces these carrier rows in the * generated constraints obtain their coordinate by type, not by a
* compiled equilibrium problem, so impose its required carrier value * hard-coded whole-model layout.
* exactly after bulk projection instead of treating that geometric
* mismatch as part of the initial residual.
*/ */
mfem::Vector enthalpy = stateView.block(utils::blocks::enthalpy_field.specific_term); RadialProjectionState projectedState{
const dimensions::SpecificEnthalpyValue requiredSurfaceEnthalpy = .density = stateView.block(utils::blocks::density_field.mass_term),
eos::evaluate<dimensions::quantity::SpecificEnthalpy>( .surfaceShape = stateView.block(utils::blocks::surface_deformation_field.parameters_term),
problem.GetStellarModel().template specification<eos::Polytrope>(), .gravityGradient = stateView.block(utils::blocks::gravity_field.gradient_term),
problem.GetStellarModel().template specification<surface::Isobaric>().targetPressure() .gravityPotential = stateView.block(utils::blocks::gravity_field.poisson_term),
); .specificEnthalpy = stateView.block(utils::blocks::enthalpy_field.specific_term)
for (const int surfaceRow : problem.GetPressureSurfaceRows().reduced_dofs()) { };
enthalpy(surfaceRow) = requiredSurfaceEnthalpy.value(); const RadialProjectionScales scales{
} .targetMass = targetMass,
.stellarRadius = profile.stellarRadius,
mfem::Vector fixedMassCoordinate = .bernoulliConstant = fields.bernoulliConstant,
stateView.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term); .sphericalMomentOfInertia = fields.sphericalMomentOfInertia,
if (fixedMassCoordinate.Size() != 1) { .surfaceCarrierRows = &problem.GetPressureSurfaceRows().reduced_dofs()
throw std::invalid_argument("FixedTotalMass must generate exactly one equilibrium-state coordinate."); };
} Projection::initialize(stellarModel, scales, projectedState, stateView);
fixedMassCoordinate(0) = fields.bernoulliConstant;
if constexpr (std::remove_cvref_t<Model>::template containsSpecification<models::FixedCentralDensity>) {
stateView.block(utils::blocks::fixed_central_density_phase.central_value_term) = 0.0;
}
return {.values = std::move(values)}; return {.values = std::move(values)};
} }
template < template <
equilibrium::StellarEquilibriumModel Model, equilibrium::StellarEquilibriumModel Model,
equilibrium::StellarDiscretizationType Discretization,
typename Strategy> typename Strategy>
requires RadialSeedStrategyFor< requires RadialSeedStrategyFor<
Strategy, Strategy,
typename equilibrium::StellarEquilibriumProblem<Model>::ModelType> typename equilibrium::StellarEquilibriumProblem<Model, Discretization>::ModelType> &&
RadialProfileProjectableModel<Model>
[[nodiscard]] ProjectedEquilibriumState<Model> makeProjectedEquilibriumState( [[nodiscard]] ProjectedEquilibriumState<Model> makeProjectedEquilibriumState(
const equilibrium::StellarEquilibriumProblem<Model> &problem, const equilibrium::StellarEquilibriumProblem<Model, Discretization> &problem,
const Strategy &strategy, const Strategy &strategy,
const StellarEquilibriumProjectionOptions &options = {} const StellarEquilibriumProjectionOptions &options = {}
) { ) {

View File

@@ -136,6 +136,19 @@ export namespace mean_field::utils::blocks {
static inline constexpr central_value central_value_term{}; static inline constexpr central_value central_value_term{};
}; };
struct fixed_angular_momentum final : field {
using SpecificationType = models::FixedAngularMomentum;
using CoordinateType = models::PhysicalCoordinateFor<SpecificationType>;
using ResidualType = models::ResidualFor<SpecificationType>;
struct angular_velocity final : term {
using value = generated_value_block<CoordinateType>;
using residual = generated_residual_block<ResidualType>;
};
static inline constexpr angular_velocity angular_velocity_term{};
};
// Compatibility name for the current barotropic formulation. The scalar // Compatibility name for the current barotropic formulation. The scalar
// is generated by FixedTotalMass; its realization in this formulation is // is generated by FixedTotalMass; its realization in this formulation is
// the historical C coordinate. // the historical C coordinate.
@@ -148,6 +161,7 @@ export namespace mean_field::utils::blocks {
inline constexpr enthalpy enthalpy_field{}; inline constexpr enthalpy enthalpy_field{};
inline constexpr fixed_total_mass fixed_total_mass_constraint{}; inline constexpr fixed_total_mass fixed_total_mass_constraint{};
inline constexpr fixed_central_density fixed_central_density_phase{}; inline constexpr fixed_central_density fixed_central_density_phase{};
inline constexpr fixed_angular_momentum fixed_angular_momentum_constraint{};
inline constexpr barotropic_constant barotropic_constant_field{}; inline constexpr barotropic_constant barotropic_constant_field{};
template <typename... Types> struct type_list { template <typename... Types> struct type_list {
@@ -516,6 +530,7 @@ export namespace mean_field::utils::blocks {
enthalpy::specific::value, enthalpy::specific::value,
gravity::poisson::value, gravity::poisson::value,
surface_deformation::parameters::value, surface_deformation::parameters::value,
density::mass::value,
barotropic_constant::mass_normalization::value>, barotropic_constant::mass_normalization::value>,
block_row< block_row<
barotropic_constant::mass_normalization::residual, barotropic_constant::mass_normalization::residual,
@@ -569,6 +584,7 @@ export namespace mean_field::utils::blocks {
enthalpy::specific::value, enthalpy::specific::value,
gravity::poisson::value, gravity::poisson::value,
surface_deformation::parameters::value, surface_deformation::parameters::value,
density::mass::value,
barotropic_constant::mass_normalization::value, barotropic_constant::mass_normalization::value,
fixed_central_density::central_value::value>, fixed_central_density::central_value::value>,
block_row< block_row<

File diff suppressed because it is too large Load Diff

View File

@@ -85,6 +85,8 @@ TEST_CASE(
STATIC_REQUIRE(field::FieldTag<field::BarotropicConstant>); STATIC_REQUIRE(field::FieldTag<field::BarotropicConstant>);
STATIC_REQUIRE(field::FieldTag<field::AngularVelocity>);
STATIC_REQUIRE_FALSE(field::FieldTag<field_registry_test_utils::MissingSupportField>); STATIC_REQUIRE_FALSE(field::FieldTag<field_registry_test_utils::MissingSupportField>);
STATIC_REQUIRE_FALSE(field::FieldTag<field_registry_test_utils::InvalidSupportField>); STATIC_REQUIRE_FALSE(field::FieldTag<field_registry_test_utils::InvalidSupportField>);
@@ -113,6 +115,8 @@ TEST_CASE(
STATIC_REQUIRE(field::NonSpatialField<field::BarotropicConstant>); STATIC_REQUIRE(field::NonSpatialField<field::BarotropicConstant>);
STATIC_REQUIRE(field::NonSpatialField<field::AngularVelocity>);
STATIC_REQUIRE(std::same_as<field::FieldDomainT<field::Density>, domain::Stellar>); STATIC_REQUIRE(std::same_as<field::FieldDomainT<field::Density>, domain::Stellar>);
STATIC_REQUIRE(std::same_as<field::FieldDomainT<field::Enthalpy>, domain::Stellar>); STATIC_REQUIRE(std::same_as<field::FieldDomainT<field::Enthalpy>, domain::Stellar>);
@@ -123,6 +127,8 @@ TEST_CASE(
STATIC_REQUIRE(std::same_as<field::FieldSupportT<field::BarotropicConstant>, field::NonSpatialSupport>); STATIC_REQUIRE(std::same_as<field::FieldSupportT<field::BarotropicConstant>, field::NonSpatialSupport>);
STATIC_REQUIRE(std::same_as<field::FieldSupportT<field::AngularVelocity>, field::NonSpatialSupport>);
CHECK(true); CHECK(true);
} }

View File

@@ -1,3 +1,5 @@
#include <array>
#include <cmath>
#include <concepts> #include <concepts>
#include <limits> #include <limits>
#include <stdexcept> #include <stdexcept>
@@ -23,10 +25,18 @@ namespace {
mean_field::eos::Polytrope, mean_field::eos::Polytrope,
mean_field::models::FixedTotalMass>; mean_field::models::FixedTotalMass>;
using AngularMomentumPolytropicMassSpecifications = mean_field::models::SpecificationSet<
mean_field::models::FixedAngularMomentum,
mean_field::surface::Isobaric,
mean_field::eos::Polytrope,
mean_field::models::FixedTotalMass>;
using PolytropicMassModel = mean_field::model::StellarModel<PolytropicMassSpecifications>; using PolytropicMassModel = mean_field::model::StellarModel<PolytropicMassSpecifications>;
using PermutedPolytropicMassModel = mean_field::model::StellarModel<PermutedPolytropicMassSpecifications>; using PermutedPolytropicMassModel = mean_field::model::StellarModel<PermutedPolytropicMassSpecifications>;
using CentralDensityPolytropicMassModel = using CentralDensityPolytropicMassModel =
mean_field::model::StellarModel<CentralDensityPolytropicMassSpecifications>; mean_field::model::StellarModel<CentralDensityPolytropicMassSpecifications>;
using AngularMomentumPolytropicMassModel =
mean_field::model::StellarModel<AngularMomentumPolytropicMassSpecifications>;
} // namespace } // namespace
TEST_CASE( TEST_CASE(
@@ -36,11 +46,13 @@ TEST_CASE(
STATIC_CHECK(mean_field::models::ModelSpecification<mean_field::eos::Polytrope>); STATIC_CHECK(mean_field::models::ModelSpecification<mean_field::eos::Polytrope>);
STATIC_CHECK(mean_field::models::ModelSpecification<mean_field::surface::ConstantPressureSurface>); STATIC_CHECK(mean_field::models::ModelSpecification<mean_field::surface::ConstantPressureSurface>);
STATIC_CHECK(mean_field::models::ModelSpecification<mean_field::models::FixedTotalMass>); STATIC_CHECK(mean_field::models::ModelSpecification<mean_field::models::FixedTotalMass>);
STATIC_CHECK(mean_field::models::ModelSpecification<mean_field::models::FixedAngularMomentum>);
STATIC_CHECK(mean_field::models::ModelSpecification<mean_field::models::FixedCentralDensity>); STATIC_CHECK(mean_field::models::ModelSpecification<mean_field::models::FixedCentralDensity>);
STATIC_CHECK_FALSE(mean_field::models::ModelSpecification<NotAModelSpecification>); STATIC_CHECK_FALSE(mean_field::models::ModelSpecification<NotAModelSpecification>);
STATIC_CHECK(mean_field::models::ResolvedModelSpecification<mean_field::eos::Polytrope>); STATIC_CHECK(mean_field::models::ResolvedModelSpecification<mean_field::eos::Polytrope>);
STATIC_CHECK(mean_field::models::ResolvedModelSpecification<mean_field::surface::ConstantPressureSurface>); STATIC_CHECK(mean_field::models::ResolvedModelSpecification<mean_field::surface::ConstantPressureSurface>);
STATIC_CHECK(mean_field::models::ResolvedModelSpecification<mean_field::models::FixedTotalMass>); STATIC_CHECK(mean_field::models::ResolvedModelSpecification<mean_field::models::FixedTotalMass>);
STATIC_CHECK(mean_field::models::ResolvedModelSpecification<mean_field::models::FixedAngularMomentum>);
STATIC_CHECK(mean_field::models::ResolvedModelSpecification<mean_field::models::FixedCentralDensity>); STATIC_CHECK(mean_field::models::ResolvedModelSpecification<mean_field::models::FixedCentralDensity>);
STATIC_CHECK( STATIC_CHECK(
@@ -61,8 +73,10 @@ TEST_CASE(
STATIC_CHECK(std::same_as<PolytropicMassModel, PermutedPolytropicMassModel>); STATIC_CHECK(std::same_as<PolytropicMassModel, PermutedPolytropicMassModel>);
STATIC_CHECK_FALSE(std::same_as<PolytropicMassModel, CentralDensityPolytropicMassModel>); STATIC_CHECK_FALSE(std::same_as<PolytropicMassModel, CentralDensityPolytropicMassModel>);
STATIC_CHECK_FALSE(std::same_as<PolytropicMassModel, AngularMomentumPolytropicMassModel>);
STATIC_CHECK(mean_field::model::StellarModelType<PolytropicMassModel>); STATIC_CHECK(mean_field::model::StellarModelType<PolytropicMassModel>);
STATIC_CHECK(mean_field::model::StellarModelType<CentralDensityPolytropicMassModel>); STATIC_CHECK(mean_field::model::StellarModelType<CentralDensityPolytropicMassModel>);
STATIC_CHECK(mean_field::model::StellarModelType<AngularMomentumPolytropicMassModel>);
} }
TEST_CASE( TEST_CASE(
@@ -81,6 +95,22 @@ TEST_CASE(
typename MassSignature::GeneratedValues> typename MassSignature::GeneratedValues>
); );
using AngularMomentumSignature = AngularMomentumPolytropicMassModel::OperatorSignature;
STATIC_CHECK(AngularMomentumSignature::generatedValueArity == 2);
STATIC_CHECK(AngularMomentumSignature::generatedResidualArity == 2);
STATIC_CHECK(AngularMomentumSignature::symbolicallySquare);
STATIC_CHECK(
mean_field::models::modelTypeListContains<
mean_field::models::PhysicalCoordinateFor<mean_field::models::FixedAngularMomentum>,
typename AngularMomentumSignature::GeneratedValues>
);
STATIC_CHECK(
mean_field::models::modelTypeListContains<
mean_field::models::ResidualFor<mean_field::models::FixedAngularMomentum>,
typename AngularMomentumSignature::GeneratedResiduals>
);
STATIC_CHECK( STATIC_CHECK(
mean_field::models::modelTypeListContains< mean_field::models::modelTypeListContains<
mean_field::models::ResidualFor<mean_field::models::FixedTotalMass>, mean_field::models::ResidualFor<mean_field::models::FixedTotalMass>,
@@ -106,6 +136,76 @@ TEST_CASE(
); );
} }
TEST_CASE(
"Fixed Angular Momentum Compiles A Physical Angular Velocity And Invariant Row",
tags::model_specification_type_contract
) {
using namespace mean_field;
using Request = models::FixedAngularMomentumLayoutRequest;
using Form = operators::CompiledStellarEquilibriumForm<AngularMomentumPolytropicMassModel>;
using Jacobian = operators::CompiledStellarEquilibriumJacobianForm<AngularMomentumPolytropicMassModel>;
using AngularValue = utils::blocks::fixed_angular_momentum::angular_velocity::value;
using AngularResidual = utils::blocks::fixed_angular_momentum::angular_velocity::residual;
STATIC_CHECK(models::ConstraintLayoutRequestType<Request>);
STATIC_CHECK(models::CompiledConstraint<models::CompiledFixedAngularMomentum>);
STATIC_CHECK(std::same_as<
typename Request::GeneratedValueType,
models::PhysicalCoordinateFor<models::FixedAngularMomentum>>);
STATIC_CHECK(std::same_as<
typename AngularValue::GeneratedType,
models::PhysicalCoordinateFor<models::FixedAngularMomentum>>);
STATIC_CHECK(std::same_as<
typename models::CompiledFixedAngularMomentum::AngularVelocityField,
field::AngularVelocity>);
STATIC_CHECK(Form::value_block_count == 7);
STATIC_CHECK(Form::residual_block_count == 7);
STATIC_CHECK(Request::valueBlock<Form>().index == 6);
STATIC_CHECK(Request::residualBlock<Form>().index == 6);
STATIC_CHECK(utils::blocks::valid_jacobian_form<Form, Jacobian>);
STATIC_CHECK(utils::blocks::has_jacobian_coupling_v<
AngularResidual,
utils::blocks::density::mass::value,
Jacobian>);
STATIC_CHECK(utils::blocks::has_jacobian_coupling_v<
AngularResidual,
utils::blocks::surface_deformation::parameters::value,
Jacobian>);
STATIC_CHECK(utils::blocks::has_jacobian_coupling_v<AngularResidual, AngularValue, Jacobian>);
STATIC_CHECK(utils::blocks::has_jacobian_coupling_v<
utils::blocks::surface_deformation::shape_equilibrium::residual,
AngularValue,
Jacobian>);
STATIC_CHECK(utils::blocks::has_jacobian_coupling_v<
utils::blocks::enthalpy::specific::residual,
AngularValue,
Jacobian>);
STATIC_CHECK_FALSE(utils::blocks::has_jacobian_coupling_v<
utils::blocks::gravity::poisson::residual,
AngularValue,
Jacobian>);
const integral::FixedAngularMomentum specification({
.Jtotal = dimensions::AngularMomentumValue{2.75},
.axis = {0.0, 3.0, 4.0},
.center = {0.25, -0.5, 0.75}
});
const models::CompiledFixedAngularMomentum compiled = models::compileConstraint(specification);
CHECK(compiled.targetAngularMomentum() == dimensions::AngularMomentumValue{2.75});
CHECK(std::abs(compiled.specification().axis()[0]) < 1.0e-15);
CHECK(std::abs(compiled.specification().axis()[1] - 0.6) < 1.0e-15);
CHECK(std::abs(compiled.specification().axis()[2] - 0.8) < 1.0e-15);
CHECK(compiled.specification().center() == std::array<double, 3>{0.25, -0.5, 0.75});
const physics::RigidRotation rotation = compiled.makeRotation(1.5);
CHECK(std::abs(rotation.angular_velocity()(0)) < 1.0e-15);
CHECK(std::abs(rotation.angular_velocity()(1) - 0.9) < 1.0e-15);
CHECK(std::abs(rotation.angular_velocity()(2) - 1.2) < 1.0e-15);
CHECK(rotation.center()(0) == 0.25);
CHECK(rotation.center()(1) == -0.5);
CHECK(rotation.center()(2) == 0.75);
}
TEST_CASE( TEST_CASE(
"Fixed Total Mass Compiles Its Generated Multiplier And Canonical Residual Row", "Fixed Total Mass Compiles Its Generated Multiplier And Canonical Residual Row",
tags::model_specification_type_contract tags::model_specification_type_contract
@@ -146,6 +246,8 @@ TEST_CASE(
constexpr auto surface = constexpr auto surface =
mean_field::models::specificationDescriptor<mean_field::surface::ConstantPressureSurface>(); mean_field::models::specificationDescriptor<mean_field::surface::ConstantPressureSurface>();
constexpr auto mass = mean_field::models::specificationDescriptor<mean_field::models::FixedTotalMass>(); constexpr auto mass = mean_field::models::specificationDescriptor<mean_field::models::FixedTotalMass>();
constexpr auto angularMomentum =
mean_field::models::specificationDescriptor<mean_field::models::FixedAngularMomentum>();
constexpr auto centralDensity = constexpr auto centralDensity =
mean_field::models::specificationDescriptor<mean_field::models::FixedCentralDensity>(); mean_field::models::specificationDescriptor<mean_field::models::FixedCentralDensity>();
@@ -162,6 +264,11 @@ TEST_CASE(
STATIC_CHECK(mass.generatedValueArity == 1); STATIC_CHECK(mass.generatedValueArity == 1);
STATIC_CHECK(mass.generatedResidualArity == 1); STATIC_CHECK(mass.generatedResidualArity == 1);
STATIC_CHECK(angularMomentum.name == "FixedAngularMomentum");
STATIC_CHECK(angularMomentum.role == mean_field::models::SpecificationRole::invariant);
STATIC_CHECK(angularMomentum.generatedValueArity == 1);
STATIC_CHECK(angularMomentum.generatedResidualArity == 1);
STATIC_CHECK(centralDensity.name == "FixedCentralDensity"); STATIC_CHECK(centralDensity.name == "FixedCentralDensity");
STATIC_CHECK(centralDensity.role == mean_field::models::SpecificationRole::phase_condition); STATIC_CHECK(centralDensity.role == mean_field::models::SpecificationRole::phase_condition);
STATIC_CHECK(centralDensity.generatedValueArity == 1); STATIC_CHECK(centralDensity.generatedValueArity == 1);
@@ -174,9 +281,15 @@ TEST_CASE(
) { ) {
const mean_field::models::FixedTotalMass mass{mean_field::dimensions::MassValue{1.25}}; const mean_field::models::FixedTotalMass mass{mean_field::dimensions::MassValue{1.25}};
const mean_field::models::FixedCentralDensity centralDensity{mean_field::eos::DensityValue{2.5}}; const mean_field::models::FixedCentralDensity centralDensity{mean_field::eos::DensityValue{2.5}};
const mean_field::models::FixedAngularMomentum angularMomentum{
mean_field::dimensions::AngularMomentumValue{0.75}
};
CHECK(mass.targetMass() == mean_field::dimensions::MassValue{1.25}); CHECK(mass.targetMass() == mean_field::dimensions::MassValue{1.25});
CHECK(centralDensity.targetDensity() == mean_field::eos::DensityValue{2.5}); CHECK(centralDensity.targetDensity() == mean_field::eos::DensityValue{2.5});
CHECK(angularMomentum.targetAngularMomentum() == mean_field::dimensions::AngularMomentumValue{0.75});
CHECK(angularMomentum.axis() == std::array<double, 3>{0.0, 0.0, 1.0});
CHECK(angularMomentum.center() == std::array<double, 3>{0.0, 0.0, 0.0});
CHECK_THROWS_AS(mean_field::models::FixedTotalMass{mean_field::dimensions::MassValue{0.0}}, std::invalid_argument); CHECK_THROWS_AS(mean_field::models::FixedTotalMass{mean_field::dimensions::MassValue{0.0}}, std::invalid_argument);
CHECK_THROWS_AS(mean_field::models::FixedTotalMass{mean_field::dimensions::MassValue{-1.0}}, std::invalid_argument); CHECK_THROWS_AS(mean_field::models::FixedTotalMass{mean_field::dimensions::MassValue{-1.0}}, std::invalid_argument);
@@ -184,6 +297,31 @@ TEST_CASE(
mean_field::models::FixedTotalMass{mean_field::dimensions::MassValue{std::numeric_limits<double>::infinity()}}, mean_field::models::FixedTotalMass{mean_field::dimensions::MassValue{std::numeric_limits<double>::infinity()}},
std::invalid_argument std::invalid_argument
); );
CHECK_THROWS_AS(
mean_field::models::FixedAngularMomentum{mean_field::dimensions::AngularMomentumValue{-1.0}},
std::invalid_argument
);
CHECK_THROWS_AS(
mean_field::models::FixedAngularMomentum{
mean_field::dimensions::AngularMomentumValue{std::numeric_limits<double>::infinity()}
},
std::invalid_argument
);
CHECK_THROWS_AS(
mean_field::models::FixedAngularMomentum({
.Jtotal = mean_field::dimensions::AngularMomentumValue{1.0},
.axis = {0.0, 0.0, 0.0}
}),
std::invalid_argument
);
CHECK_THROWS_AS(
mean_field::models::FixedAngularMomentum({
.Jtotal = mean_field::dimensions::AngularMomentumValue{1.0},
.axis = {0.0, 0.0, 1.0},
.center = {0.0, std::numeric_limits<double>::quiet_NaN(), 0.0}
}),
std::invalid_argument
);
CHECK_THROWS_AS(mean_field::models::FixedCentralDensity{mean_field::eos::DensityValue{0.0}}, std::invalid_argument); CHECK_THROWS_AS(mean_field::models::FixedCentralDensity{mean_field::eos::DensityValue{0.0}}, std::invalid_argument);
CHECK_THROWS_AS( CHECK_THROWS_AS(

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
#include <concepts> #include <concepts>
#include <limits> #include <limits>
#include <memory>
#include <stdexcept> #include <stdexcept>
#include <type_traits> #include <type_traits>
@@ -19,6 +20,101 @@ namespace {
mean_field::eos::Polytrope, mean_field::eos::Polytrope,
mean_field::surface::Isobaric, mean_field::surface::Isobaric,
mean_field::integral::FixedTotalMass>>; mean_field::integral::FixedTotalMass>>;
class MoveOnlyEquationOfState final {
public:
struct Parameters final {
int marker;
};
using ModelDefinition =
mean_field::eos::ConstitutiveLaw<MoveOnlyEquationOfState, "MoveOnlyEquationOfState">;
explicit MoveOnlyEquationOfState(const Parameters parameters)
: m_marker(std::make_unique<int>(parameters.marker)) {
}
MoveOnlyEquationOfState(const MoveOnlyEquationOfState &) = delete;
MoveOnlyEquationOfState &operator=(const MoveOnlyEquationOfState &) = delete;
MoveOnlyEquationOfState(MoveOnlyEquationOfState &&) noexcept = default;
MoveOnlyEquationOfState &operator=(MoveOnlyEquationOfState &&) noexcept = default;
[[nodiscard]] const std::unique_ptr<int> &marker() const noexcept {
return m_marker;
}
private:
std::unique_ptr<int> m_marker;
};
class MoveOnlySurfaceCondition final {
public:
struct Parameters final {
int marker;
};
using ModelDefinition =
mean_field::surface::BoundaryCondition<MoveOnlySurfaceCondition, "MoveOnlySurfaceCondition">;
explicit MoveOnlySurfaceCondition(const Parameters parameters)
: m_marker(std::make_unique<int>(parameters.marker)) {
}
MoveOnlySurfaceCondition(const MoveOnlySurfaceCondition &) = delete;
MoveOnlySurfaceCondition &operator=(const MoveOnlySurfaceCondition &) = delete;
MoveOnlySurfaceCondition(MoveOnlySurfaceCondition &&) noexcept = default;
MoveOnlySurfaceCondition &operator=(MoveOnlySurfaceCondition &&) noexcept = default;
[[nodiscard]] const std::unique_ptr<int> &marker() const noexcept {
return m_marker;
}
private:
std::unique_ptr<int> m_marker;
};
class MoveOnlyIntegralConstraint final {
public:
struct Parameters final {
int marker;
};
using ScalarDescription = mean_field::stellar::ScalarConstraint<
mean_field::dimensions::quantity::Mass,
mean_field::dimensions::quantity::SpecificEnergy,
mean_field::dimensions::quantity::Mass,
"move_only_integral.multiplier",
"C_move",
"move_only_integral.residual",
"R_move">;
using TargetValue = typename ScalarDescription::TargetValue;
using ModelDefinition = mean_field::integral::FixedScalarWithMultiplier<
MoveOnlyIntegralConstraint,
"MoveOnlyIntegralConstraint",
mean_field::stellar::Reads<mean_field::stellar::state::Density>,
mean_field::stellar::Changes<mean_field::stellar::equation::HydrostaticBalance>,
ScalarDescription>;
explicit MoveOnlyIntegralConstraint(const Parameters parameters)
: m_marker(std::make_unique<int>(parameters.marker)) {
}
MoveOnlyIntegralConstraint(const MoveOnlyIntegralConstraint &) = delete;
MoveOnlyIntegralConstraint &operator=(const MoveOnlyIntegralConstraint &) = delete;
MoveOnlyIntegralConstraint(MoveOnlyIntegralConstraint &&) noexcept = default;
MoveOnlyIntegralConstraint &operator=(MoveOnlyIntegralConstraint &&) noexcept = default;
[[nodiscard]] const std::unique_ptr<int> &marker() const noexcept {
return m_marker;
}
[[nodiscard]] TargetValue target() const noexcept {
return TargetValue{static_cast<double>(*m_marker)};
}
private:
std::unique_ptr<int> m_marker;
};
} // namespace } // namespace
TEST_CASE( TEST_CASE(
@@ -38,8 +134,8 @@ TEST_CASE(
STATIC_CHECK(models::SpecifiedModelType<decltype(stellarModel)>); STATIC_CHECK(models::SpecifiedModelType<decltype(stellarModel)>);
STATIC_CHECK(CanonicalModel::specificationCount == 4); STATIC_CHECK(CanonicalModel::specificationCount == 4);
STATIC_CHECK(CanonicalModel::symbolicallySquare); STATIC_CHECK(CanonicalModel::symbolicallySquare);
STATIC_CHECK(CanonicalModel::hasCompleteEquilibriumCompiler); STATIC_CHECK(CanonicalModel::hasCompleteEquilibriumDeclaration);
STATIC_CHECK(CanonicalModel::compilationClass == models::EquilibriumSystemCompilation::complete_equilibrium_system); STATIC_CHECK(operators::StellarEquilibriumSystemCompilable<CanonicalModel>);
CHECK(stellarModel.specification<eos::Polytrope>().polytropic_index() == 3.0); CHECK(stellarModel.specification<eos::Polytrope>().polytropic_index() == 3.0);
CHECK(stellarModel.specification<eos::Polytrope>().polytropic_constant() == 0.25); CHECK(stellarModel.specification<eos::Polytrope>().polytropic_constant() == 0.25);
@@ -50,6 +146,44 @@ TEST_CASE(
); );
} }
TEST_CASE(
"Incomplete Stellar Model Spellings Fail Capability Probes Without Diagnostics",
tags::stellar_model_specification_api
) {
using namespace mean_field;
using IncompleteModel = model::StellarModel<int>;
STATIC_CHECK_FALSE(model::StellarModelType<IncompleteModel>);
STATIC_CHECK_FALSE(models::SpecifiedModelType<IncompleteModel>);
STATIC_CHECK(model::specificationRoleCount<models::SpecificationRole::constitutive_law, IncompleteModel> == 0);
STATIC_CHECK_FALSE(model::HasEquationOfState<IncompleteModel>);
STATIC_CHECK_FALSE(model::HasSurfaceCondition<IncompleteModel>);
STATIC_CHECK_FALSE(operators::StellarEquilibriumSystemCompilable<IncompleteModel>);
STATIC_CHECK_FALSE(equilibrium::StellarEquilibriumModel<IncompleteModel>);
}
TEST_CASE(
"Specification Sets Canonicalize Cvref-Qualified Physics Types",
tags::stellar_model_specification_api
) {
using namespace mean_field;
using Qualified = models::SpecificationSet<
const eos::Polytrope &,
volatile surface::Isobaric &&,
const integral::FixedTotalMass>;
using Plain = models::SpecificationSet<
eos::Polytrope,
surface::Isobaric,
integral::FixedTotalMass>;
STATIC_CHECK(std::same_as<Qualified, Plain>);
STATIC_CHECK(models::ValidModelSpecificationPack<
const eos::Polytrope &,
volatile surface::Isobaric &&,
const integral::FixedTotalMass>);
STATIC_CHECK(model::StellarModelType<model::StellarModel<Qualified>>);
}
TEST_CASE( TEST_CASE(
"Stellar Model Deduction Canonicalizes Unordered Specifications", "Stellar Model Deduction Canonicalizes Unordered Specifications",
tags::stellar_model_specification_api tags::stellar_model_specification_api
@@ -85,6 +219,40 @@ TEST_CASE(
CHECK(descriptors[3].specification.name == "FixedCentralDensity"); CHECK(descriptors[3].specification.name == "FixedCentralDensity");
} }
TEST_CASE(
"Canonical Model Construction Preserves Every Move-Only Physics Specification",
tags::stellar_model_specification_api
) {
using namespace mean_field;
auto stellarModel = model::StellarModel(
MoveOnlyIntegralConstraint({.marker = 307}),
MoveOnlySurfaceCondition({.marker = 211}),
MoveOnlyEquationOfState({.marker = 101})
);
using Model = std::remove_cvref_t<decltype(stellarModel)>;
using Expected = model::StellarModel<models::SpecificationSet<
MoveOnlyEquationOfState,
MoveOnlySurfaceCondition,
MoveOnlyIntegralConstraint>>;
STATIC_CHECK(std::same_as<Model, Expected>);
STATIC_CHECK_FALSE(std::copy_constructible<MoveOnlyEquationOfState>);
STATIC_CHECK_FALSE(std::copy_constructible<MoveOnlySurfaceCondition>);
STATIC_CHECK_FALSE(std::copy_constructible<MoveOnlyIntegralConstraint>);
const auto &equationOfState = stellarModel.specification<MoveOnlyEquationOfState>();
const auto &surfaceCondition = stellarModel.specification<MoveOnlySurfaceCondition>();
const auto &integralConstraint = stellarModel.specification<MoveOnlyIntegralConstraint>();
REQUIRE(equationOfState.marker() != nullptr);
REQUIRE(surfaceCondition.marker() != nullptr);
REQUIRE(integralConstraint.marker() != nullptr);
CHECK(*equationOfState.marker() == 101);
CHECK(*surfaceCondition.marker() == 211);
CHECK(*integralConstraint.marker() == 307);
}
TEST_CASE( TEST_CASE(
"Stellar Specification Parameter Constructors Preserve Validation", "Stellar Specification Parameter Constructors Preserve Validation",
tags::stellar_model_specification_api tags::stellar_model_specification_api

View File

@@ -1,8 +1,12 @@
#include <algorithm> #include <algorithm>
#include <catch2/catch_test_macros.hpp> #include <catch2/catch_test_macros.hpp>
#include <cmath> #include <cmath>
#include <cstdint>
#include <limits> #include <limits>
#include <mfem.hpp> #include <mfem.hpp>
#include <numbers>
#include <stdexcept>
#include <type_traits>
#include <vector> #include <vector>
import mean_field; import mean_field;
@@ -30,12 +34,41 @@ namespace {
return vector; return vector;
} }
void require_all_ranks(
const bool localCondition,
const MPI_Comm communicator,
const char *description
) {
int rank = 0;
int size = 0;
MPI_Comm_rank(communicator, &rank);
MPI_Comm_size(communicator, &size);
const int localFailure = localCondition ? size : rank;
int firstFailure = size;
const int result = MPI_Allreduce(
&localFailure,
&firstFailure,
1,
MPI_INT,
MPI_MIN,
communicator
);
REQUIRE(result == MPI_SUCCESS);
CAPTURE(description, localCondition, firstFailure);
REQUIRE(firstFailure == size);
}
double global_dot( double global_dot(
const mfem::Vector &left, const mfem::Vector &left,
const mfem::Vector &right, const mfem::Vector &right,
const MPI_Comm communicator const MPI_Comm communicator
) { ) {
REQUIRE(left.Size() == right.Size()); require_all_ranks(
left.Size() == right.Size(),
communicator,
"global dot-product vector sizes"
);
const double local = left * right; const double local = left * right;
double global = 0.0; double global = 0.0;
REQUIRE(MPI_Allreduce(&local, &global, 1, MPI_DOUBLE, MPI_SUM, communicator) == MPI_SUCCESS); REQUIRE(MPI_Allreduce(&local, &global, 1, MPI_DOUBLE, MPI_SUM, communicator) == MPI_SUCCESS);
@@ -48,6 +81,39 @@ namespace {
) { ) {
return std::sqrt(global_dot(vector, vector, communicator)); return std::sqrt(global_dot(vector, vector, communicator));
} }
[[nodiscard]] mean_field::operators::StellarEquilibriumDependencies
make_stellar_dependencies(const std::uint64_t revision = 1) {
return {
.discretization = {.identity = 16101, .revision = 1},
.density = {.identity = 16103, .revision = revision},
.surfaceDeformation = {.identity = 16111, .revision = revision},
.gravityGradient = {.identity = 16127, .revision = revision},
.gravityPotential = {.identity = 16139, .revision = revision},
.enthalpy = {.identity = 16141, .revision = revision},
.bernoulliConstant = {.identity = 16183, .revision = revision},
.rotation = {.identity = 16187, .revision = revision},
.targetMass = {.identity = 16189, .revision = 1}
};
}
void check_rank_consistent_scalar(
const double value,
const MPI_Comm communicator,
const double relativeTolerance = 2.0e-13
) {
double minimum = 0.0;
double maximum = 0.0;
REQUIRE(MPI_Allreduce(&value, &minimum, 1, MPI_DOUBLE, MPI_MIN, communicator) == MPI_SUCCESS);
REQUIRE(MPI_Allreduce(&value, &maximum, 1, MPI_DOUBLE, MPI_MAX, communicator) == MPI_SUCCESS);
CAPTURE(value, minimum, maximum);
CHECK(std::isfinite(minimum));
CHECK(std::isfinite(maximum));
CHECK(
std::abs(maximum - minimum) <=
relativeTolerance * std::max({1.0, std::abs(minimum), std::abs(maximum)})
);
}
} // namespace } // namespace
TEST_CASE( TEST_CASE(
@@ -97,6 +163,750 @@ TEST_CASE(
CHECK(f.logicalReferenceMesh->GetNE() == f.mesh->GetNE()); CHECK(f.logicalReferenceMesh->GetNE() == f.mesh->GetNE());
} }
TEST_CASE(
"MPI Fixed Angular Momentum Produces One Consistent Global Invariant Row",
"[mpi][distributed][fixed-angular-momentum][physics][jacobian]"
) {
using namespace mean_field;
const auto args = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(args.mesh_file, args, 0);
require_all_ranks(
finiteElements.okay(),
MPI_COMM_WORLD,
"fixed-angular-momentum FEM setup"
);
const MPI_Comm communicator = finiteElements.mesh->GetComm();
mfem::ParGridFunction densityField(finiteElements.densityFes.get());
mfem::ConstantCoefficient densityCoefficient(1.23);
densityField.ProjectCoefficient(densityCoefficient);
mfem::Vector densityTrue;
densityField.GetTrueDofs(densityTrue);
mfem::Vector displacementTrue(finiteElements.displacementFes->GetTrueVSize());
displacementTrue = 0.0;
finiteElements.displacement->SetFromTrueDofs(displacementTrue);
mfem::Vector gravityGradientTrue(finiteElements.gravityFluxFes->GetTrueVSize());
mfem::Vector gravityPotentialTrue(finiteElements.gravityPotentialFes->GetTrueVSize());
gravityGradientTrue = 0.0;
gravityPotentialTrue = 0.0;
operators::context::gravity_field::GravityFieldLinearizationContext gravityContext(
finiteElements,
*finiteElements.domainMapperStateless
);
gravityContext.Prepare(
{.density = gravityContext.GetDensityMap().gather(densityTrue),
.displacement = gravityContext.GetDisplacementMap().gather(displacementTrue),
.gravity_gradient = gravityContext.GetGravityGradientMap().gather(gravityGradientTrue),
.gravity_potential = gravityContext.GetGravityPotentialMap().gather(gravityPotentialTrue)},
{.discretization = {.value = 2},
.displacement = {.value = 3},
.density = {.value = 5},
.gravity_gradient = {.value = 7},
.gravity_potential = {.value = 11}}
);
constexpr double targetAngularMomentum = 0.37;
constexpr double angularVelocity = 0.61;
operators::PreparedAngularMomentumOperator invariant(
finiteElements,
*finiteElements.domainMapperStateless,
gravityContext,
models::compileConstraint(
integral::FixedAngularMomentum({.Jtotal = dimensions::AngularMomentumValue{targetAngularMomentum}})
)
);
const operators::AngularMomentumDependencies dependencies{
.discretization = {.identity = 101, .revision = 2},
.density = {.identity = 103, .revision = 5},
.displacement = {.identity = 107, .revision = 3},
.rotation = {.identity = 109, .revision = 13}
};
const auto preparation = invariant.Prepare(angularVelocity, dependencies);
CHECK(preparation.rebuiltStaticPlan);
CHECK(preparation.refreshedGeometry);
CHECK(preparation.refreshedDensity);
CHECK(preparation.updatedAngularVelocity);
CHECK(preparation.assembledResidual);
const double independentMoment = analysis::get_moment_of_inertia(finiteElements, densityField);
const double preparedMoment = invariant.GetMomentOfInertia();
const double comparisonScale = std::max({std::abs(independentMoment), std::abs(preparedMoment), 1.0e-300});
CHECK(std::abs(preparedMoment - independentMoment) / comparisonScale <= 3.0e-13);
double minimumMoment = 0.0;
double maximumMoment = 0.0;
MPI_Allreduce(&preparedMoment, &minimumMoment, 1, MPI_DOUBLE, MPI_MIN, finiteElements.mesh->GetComm());
MPI_Allreduce(&preparedMoment, &maximumMoment, 1, MPI_DOUBLE, MPI_MAX, finiteElements.mesh->GetComm());
CHECK(std::abs(maximumMoment - minimumMoment) / comparisonScale <= 2.0e-15);
mfem::Vector residual;
invariant.BuildResidual(residual);
require_all_ranks(
residual.Size() == 1,
communicator,
"fixed-angular-momentum residual size"
);
CHECK(
std::abs(residual(0) - (angularVelocity * independentMoment - targetAngularMomentum)) /
std::max({std::abs(residual(0)), std::abs(angularVelocity * independentMoment), 1.0}) <=
3.0e-13
);
mfem::Vector densityAction;
invariant.ApplyDensityJacobianAction(gravityContext.GetDensityMap().gather(densityTrue), densityAction);
require_all_ranks(
densityAction.Size() == 1,
communicator,
"fixed-angular-momentum density action size"
);
CHECK(std::abs(densityAction(0) - angularVelocity * preparedMoment) / comparisonScale <= 3.0e-13);
constexpr double angularVelocityVariation = -0.29;
mfem::Vector angularVelocityAction;
invariant.ApplyAngularVelocityJacobianAction(angularVelocityVariation, angularVelocityAction);
require_all_ranks(
angularVelocityAction.Size() == 1,
communicator,
"fixed-angular-momentum rotation action size"
);
CHECK(
std::abs(angularVelocityAction(0) - angularVelocityVariation * preparedMoment) / comparisonScale <=
2.0e-15
);
}
TEST_CASE(
"MPI Density Volume Context Forwards The Prepared Global Mass Integral",
"[mpi][distributed][integral-context][physics-extension]"
) {
using namespace mean_field;
const utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
require_all_ranks(
finiteElements.okay(),
MPI_COMM_WORLD,
"density-volume context FEM setup"
);
const MPI_Comm communicator = finiteElements.mesh->GetComm();
constexpr double densityValue = 1.23;
mfem::ParGridFunction densityField(finiteElements.densityFes.get());
mfem::ConstantCoefficient densityCoefficient(densityValue);
densityField.ProjectCoefficient(densityCoefficient);
mfem::Vector densityTrue;
densityField.GetTrueDofs(densityTrue);
mfem::Vector displacementTrue(finiteElements.displacementFes->GetTrueVSize());
displacementTrue = 0.0;
finiteElements.displacement->SetFromTrueDofs(displacementTrue);
mfem::Vector gravityGradientTrue(finiteElements.gravityFluxFes->GetTrueVSize());
mfem::Vector gravityPotentialTrue(finiteElements.gravityPotentialFes->GetTrueVSize());
gravityGradientTrue = 0.0;
gravityPotentialTrue = 0.0;
operators::context::gravity_field::GravityFieldLinearizationContext gravityContext(
finiteElements,
*finiteElements.domainMapperStateless
);
const mfem::Vector reducedDensity = gravityContext.GetDensityMap().gather(
densityTrue
);
gravityContext.Prepare(
{.density = reducedDensity,
.displacement = gravityContext.GetDisplacementMap().gather(displacementTrue),
.gravity_gradient = gravityContext.GetGravityGradientMap().gather(gravityGradientTrue),
.gravity_potential = gravityContext.GetGravityPotentialMap().gather(gravityPotentialTrue)},
{.discretization = {.value = 2},
.displacement = {.value = 3},
.density = {.value = 5},
.gravity_gradient = {.value = 7},
.gravity_potential = {.value = 11}}
);
operators::PreparedMassNormalizationOperator massIntegral(
finiteElements,
*finiteElements.domainMapperStateless,
gravityContext
);
massIntegral.Prepare(
models::compileConstraint(
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}})
),
{.discretization = {.identity = 101, .revision = 2},
.density = {.identity = 103, .revision = 5},
.displacement = {.identity = 107, .revision = 3},
.targetMass = {.identity = 109, .revision = 1}}
);
class DistributedMassIntegralCore final {
public:
explicit DistributedMassIntegralCore(
const operators::PreparedMassNormalizationOperator &mass
) noexcept
: m_mass(&mass) {
}
[[nodiscard]] double ApplyDensityVolumeIntegralDensityAction(
const mfem::Vector &direction
) const {
mfem::Vector action;
m_mass->ApplyDensityJacobianAction(direction, action);
return action(0);
}
[[nodiscard]] double ApplyDensityVolumeIntegralSurfaceShapeAction(
const mfem::Vector &
) const noexcept {
return 0.0;
}
private:
const operators::PreparedMassNormalizationOperator *m_mass;
};
const DistributedMassIntegralCore testCore{massIntegral};
const stellar::DensityVolumeIntegralContext<integral::FixedTotalMass>
densityIntegral{testCore};
const double integratedMass =
densityIntegral.integrateDensity(reducedDensity).value();
const double linearizedDensityMass =
densityIntegral.linearizeDensityIntegral(reducedDensity).value();
const double preparedMass = massIntegral.GetCurrentMass();
const double independentMass =
densityValue * analysis::get_mesh_volume(finiteElements);
check_rank_consistent_scalar(integratedMass, communicator);
check_rank_consistent_scalar(linearizedDensityMass, communicator);
check_rank_consistent_scalar(preparedMass, communicator);
check_rank_consistent_scalar(independentMass, communicator);
const double massScale = std::max(
{1.0, std::abs(integratedMass), std::abs(independentMass)}
);
CHECK(std::abs(integratedMass - independentMass) <= 3.0e-12 * massScale);
CHECK(std::abs(integratedMass - preparedMass) <= 3.0e-13 * massScale);
CHECK(std::abs(linearizedDensityMass - integratedMass) <=
3.0e-13 * massScale);
}
TEST_CASE(
"MPI Assembled Variadic Stellar Root Normalizes And Applies Its Inferred Preconditioner",
"[mpi][distributed][stellar-equilibrium][normalization][preconditioning][integration]"
) {
using namespace mean_field;
const utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
require_all_ranks(
finiteElements.okay(),
MPI_COMM_WORLD,
"variadic stellar-root FEM setup"
);
const MPI_Comm communicator = finiteElements.mesh->GetComm();
constexpr double radius = utils::RADIUS;
constexpr double mass = utils::MASS;
const double polytropicConstant =
2.0 * utils::G * radius * radius / std::numbers::pi_v<double>;
const double centralDensity =
std::numbers::pi_v<double> * mass / (4.0 * radius * radius * radius);
auto model = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}),
integral::FixedAngularMomentum({
.Jtotal = dimensions::AngularMomentumValue{0.05},
.axis = {0.0, 0.0, 1.0}
}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
);
auto problem = equilibrium::discretize(
model,
equilibrium::makeStellarDiscretization(
finiteElements,
normalization::PhysicalRieszDiagonal{
dimensions::LengthValue{radius},
utils::G
}
)
);
auto projected = seed::makeProjectedEquilibriumState(
problem,
seed::LaneEmden({
.centralDensity = dimensions::DensityValue{centralDensity},
.radialSampleCount = 512
})
);
auto normalized = normalization::makeNormalizedStellarEquilibriumOperator(problem);
using Problem = std::remove_cvref_t<decltype(problem)>;
using Form = typename Problem::FormType;
constexpr auto massResidualBlock = utils::blocks::get_residual_block<Form>(
utils::blocks::fixed_total_mass_constraint.mass_normalization_term
);
constexpr auto angularMomentumResidualBlock = utils::blocks::get_residual_block<Form>(
utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term
);
constexpr auto centralDensityResidualBlock = utils::blocks::get_residual_block<Form>(
utils::blocks::fixed_central_density_phase.central_value_term
);
require_all_ranks(
problem.StateSize() == problem.EquationSize(),
communicator,
"variadic stellar-root square layout"
);
mfem::Vector normalizedState;
normalized.NormalizeState(projected.values, normalizedState);
const auto preparation = normalized.Prepare(
normalizedState,
make_stellar_dependencies(1)
);
require_all_ranks(
preparation.DidAnyWork() &&
preparation.generatedPhysicalControl &&
preparation.template specification<models::FixedAngularMomentum>().generatedRotation &&
problem.IsPrepared() &&
normalized.IsPrepared(),
communicator,
"initial variadic stellar-root preparation"
);
// The astronomy-facing integral handle must delegate to the same mapped
// physical-volume quadrature and collective reduction as the prepared
// mass equation, without exposing the FEM/core objects to extension
// physics. Linearity in density gives an independent check of both
// public operations on every rank.
const auto physicalState = problem.GetManifest().stateView(projected.values);
const auto physicalDensity = physicalState.block(
utils::blocks::density_field.mass_term
);
const stellar::DensityVolumeIntegralContext<integral::FixedTotalMass>
densityIntegral{problem.GetPhysicalOperator()};
const double integratedMass = densityIntegral.integrateDensity(
physicalDensity
).value();
const double linearizedMass = densityIntegral.linearizeDensityIntegral(
physicalDensity
).value();
const double preparedMass = problem.GetPhysicalOperator()
.GetFixedMassReport()
.achieved;
check_rank_consistent_scalar(integratedMass, communicator);
check_rank_consistent_scalar(linearizedMass, communicator);
const double massComparisonScale = std::max(
{std::abs(integratedMass), std::abs(preparedMass), 1.0e-300}
);
CHECK(std::abs(integratedMass - preparedMass) / massComparisonScale <=
3.0e-13);
CHECK(std::abs(linearizedMass - preparedMass) / massComparisonScale <=
3.0e-13);
mfem::Vector normalizedResidual;
normalized.BuildResidual(normalizedResidual);
require_all_ranks(
normalizedResidual.Size() == problem.EquationSize(),
communicator,
"normalized variadic residual size"
);
auto residualView = problem.GetManifest().residualView(normalizedResidual);
const auto &layout = problem.GetManifest().layout();
const auto &residualFactors = normalized.GetNormalization().ResidualFactors();
const auto massReport = problem.GetPreparedOperator().GetFixedMassReport();
const auto angularMomentumReport =
problem.GetPreparedOperator().GetAngularMomentumReport();
const auto centralDensityReport =
problem.GetPreparedOperator().GetCentralDensityReport();
const auto checkReportedScalarResidual = [&](const mfem::Vector &block,
const double expected) {
require_all_ranks(
block.Size() == 1,
communicator,
"reported scalar residual block size"
);
const double actual = block(0);
CAPTURE(actual, expected);
CHECK(std::isfinite(expected));
CHECK(
std::abs(actual - expected) <=
5.0e-13 * std::max({1.0, std::abs(actual), std::abs(expected)})
);
check_rank_consistent_scalar(actual, finiteElements.mesh->GetComm());
};
checkReportedScalarResidual(
residualView.block(
utils::blocks::fixed_total_mass_constraint.mass_normalization_term
),
massReport.dimensionalResidual *
residualFactors(layout.offset(massResidualBlock))
);
checkReportedScalarResidual(
residualView.block(
utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term
),
angularMomentumReport.dimensionalResidual *
residualFactors(layout.offset(angularMomentumResidualBlock))
);
checkReportedScalarResidual(
residualView.block(
utils::blocks::fixed_central_density_phase.central_value_term
),
centralDensityReport.enthalpyResidual *
residualFactors(layout.offset(centralDensityResidualBlock))
);
mfem::Vector normalizedDirection = make_deterministic_vector(problem.StateSize(), 0.37);
const auto directionState = problem.GetManifest().stateView(normalizedDirection);
mfem::Vector massDirection = directionState.block(
utils::blocks::fixed_total_mass_constraint.mass_normalization_term
);
mfem::Vector angularVelocityDirection = directionState.block(
utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term
);
mfem::Vector phaseDirection = directionState.block(
utils::blocks::fixed_central_density_phase.central_value_term
);
require_all_ranks(
massDirection.Size() == 1 &&
angularVelocityDirection.Size() == 1 &&
phaseDirection.Size() == 1,
communicator,
"generated scalar direction block sizes"
);
massDirection(0) = 0.17;
angularVelocityDirection(0) = -0.23;
phaseDirection(0) = 0.31;
massDirection.SyncAliasMemory(normalizedDirection);
angularVelocityDirection.SyncAliasMemory(normalizedDirection);
phaseDirection.SyncAliasMemory(normalizedDirection);
normalizedDirection /= global_norm(normalizedDirection, finiteElements.mesh->GetComm());
mfem::Vector normalizedAction;
normalized.Mult(normalizedDirection, normalizedAction);
require_all_ranks(
normalizedAction.Size() == problem.EquationSize(),
communicator,
"normalized variadic Jacobian-action size"
);
/* Different dependency revisions are intentional: the state changes in
* each difference evaluation, so the distributed physical contexts must
* be rebuilt even though all persistent identities remain the same. */
constexpr double differenceStep = 1.0e-5;
mfem::Vector plusState(normalizedState);
plusState.Add(differenceStep, normalizedDirection);
const auto plusPreparation = normalized.Prepare(
plusState,
make_stellar_dependencies(2)
);
require_all_ranks(
plusPreparation.DidAnyWork(),
communicator,
"positive finite-difference preparation"
);
mfem::Vector plusResidual;
normalized.BuildResidual(plusResidual);
mfem::Vector minusState(normalizedState);
minusState.Add(-differenceStep, normalizedDirection);
const auto minusPreparation = normalized.Prepare(
minusState,
make_stellar_dependencies(3)
);
require_all_ranks(
minusPreparation.DidAnyWork(),
communicator,
"negative finite-difference preparation"
);
mfem::Vector minusResidual;
normalized.BuildResidual(minusResidual);
mfem::Vector finiteDifference(plusResidual);
finiteDifference -= minusResidual;
finiteDifference /= 2.0 * differenceStep;
const auto restoredPreparation = normalized.Prepare(
normalizedState,
make_stellar_dependencies(4)
);
require_all_ranks(
restoredPreparation.DidAnyWork() && normalized.IsPrepared(),
communicator,
"restored finite-difference preparation"
);
mfem::Vector restoredResidual;
normalized.BuildResidual(restoredResidual);
mfem::Vector restoredResidualDifference(restoredResidual);
restoredResidualDifference -= normalizedResidual;
const double restoredResidualError = global_norm(
restoredResidualDifference,
finiteElements.mesh->GetComm()
) / std::max({
global_norm(restoredResidual, finiteElements.mesh->GetComm()),
global_norm(normalizedResidual, finiteElements.mesh->GetComm()),
std::numeric_limits<double>::epsilon()
});
CAPTURE(restoredResidualError);
CHECK(restoredResidualError <= 2.0e-12);
mfem::Vector finiteDifferenceError(normalizedAction);
finiteDifferenceError -= finiteDifference;
const double actionNorm = global_norm(
normalizedAction,
finiteElements.mesh->GetComm()
);
const double finiteDifferenceNorm = global_norm(
finiteDifference,
finiteElements.mesh->GetComm()
);
const double completeDifferenceError = global_norm(
finiteDifferenceError,
finiteElements.mesh->GetComm()
) / std::max({
actionNorm,
finiteDifferenceNorm,
std::numeric_limits<double>::epsilon()
});
CAPTURE(actionNorm, finiteDifferenceNorm, completeDifferenceError);
CHECK(actionNorm > std::numeric_limits<double>::min());
CHECK(finiteDifferenceNorm > std::numeric_limits<double>::min());
CHECK(completeDifferenceError <= 8.0e-5);
const int locallyFinite =
vector_is_finite(normalizedResidual) &&
vector_is_finite(normalizedAction) &&
vector_is_finite(finiteDifference) ? 1 : 0;
int globallyFinite = 0;
REQUIRE(MPI_Allreduce(
&locallyFinite,
&globallyFinite,
1,
MPI_INT,
MPI_MIN,
finiteElements.mesh->GetComm()
) == MPI_SUCCESS);
CHECK(globallyFinite == 1);
CHECK(global_norm(normalizedResidual, finiteElements.mesh->GetComm()) > 0.0);
CHECK(global_norm(normalizedAction, finiteElements.mesh->GetComm()) > 0.0);
auto actionView = problem.GetManifest().residualView(normalizedAction);
auto finiteDifferenceView = problem.GetManifest().residualView(finiteDifference);
const auto checkGlobalRow = [&](const auto &term, const char *rowName) {
const mfem::Vector residualBlock = residualView.block(term);
const mfem::Vector actionBlock = actionView.block(term);
const mfem::Vector differenceBlock = finiteDifferenceView.block(term);
require_all_ranks(
residualBlock.Size() == 1 &&
actionBlock.Size() == 1 &&
differenceBlock.Size() == 1,
communicator,
"global scalar residual/Jacobian block sizes"
);
check_rank_consistent_scalar(residualBlock(0), finiteElements.mesh->GetComm());
check_rank_consistent_scalar(actionBlock(0), finiteElements.mesh->GetComm());
check_rank_consistent_scalar(differenceBlock(0), finiteElements.mesh->GetComm());
const double rowMagnitude = std::max(
std::abs(actionBlock(0)),
std::abs(differenceBlock(0))
);
const double rowDifferenceError =
std::abs(actionBlock(0) - differenceBlock(0)) /
std::max(rowMagnitude, std::numeric_limits<double>::epsilon());
CAPTURE(rowName, actionBlock(0), differenceBlock(0), rowMagnitude,
rowDifferenceError);
CHECK(rowMagnitude > 1.0e-10);
CHECK(rowDifferenceError <= 8.0e-5);
};
checkGlobalRow(
utils::blocks::fixed_total_mass_constraint.mass_normalization_term,
"fixed-total-mass"
);
checkGlobalRow(
utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term,
"fixed-angular-momentum"
);
checkGlobalRow(
utils::blocks::fixed_central_density_phase.central_value_term,
"fixed-central-density phase"
);
auto component = preconditioning::makePreconditioner(problem);
STATIC_CHECK(decltype(component)::borderValueArity == 3);
STATIC_CHECK(decltype(component)::borderResidualArity == 3);
auto physicalInverse = preconditioning::prepare(problem, component);
require_all_ranks(
physicalInverse.IsCurrent(),
communicator,
"prepared physical preconditioner currentness"
);
auto scaledInverse = normalized.MakeScaledPreconditioner(physicalInverse);
require_all_ranks(
scaledInverse.IsCurrent(),
communicator,
"prepared normalized preconditioner currentness"
);
mfem::Vector normalizedRightHandSide =
make_deterministic_vector(problem.EquationSize(), 0.73);
auto rightHandSideView = problem.GetManifest().residualView(normalizedRightHandSide);
mfem::Vector massRightHandSide = rightHandSideView.block(
utils::blocks::fixed_total_mass_constraint.mass_normalization_term
);
mfem::Vector angularMomentumRightHandSide = rightHandSideView.block(
utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term
);
mfem::Vector phaseRightHandSide = rightHandSideView.block(
utils::blocks::fixed_central_density_phase.central_value_term
);
massRightHandSide(0) = 0.11;
angularMomentumRightHandSide(0) = -0.19;
phaseRightHandSide(0) = 0.29;
massRightHandSide.SyncAliasMemory(normalizedRightHandSide);
angularMomentumRightHandSide.SyncAliasMemory(normalizedRightHandSide);
phaseRightHandSide.SyncAliasMemory(normalizedRightHandSide);
normalizedRightHandSide /= global_norm(
normalizedRightHandSide,
finiteElements.mesh->GetComm()
);
mfem::Vector firstCorrection(scaledInverse.Height());
mfem::Vector repeatedCorrection(scaledInverse.Height());
firstCorrection = 0.0;
repeatedCorrection = 0.0;
scaledInverse.Mult(normalizedRightHandSide, firstCorrection);
scaledInverse.Mult(normalizedRightHandSide, repeatedCorrection);
mfem::Vector repeatDifference(repeatedCorrection);
repeatDifference -= firstCorrection;
const double correctionNorm = global_norm(firstCorrection, finiteElements.mesh->GetComm());
const double repeatError = global_norm(repeatDifference, finiteElements.mesh->GetComm()) /
std::max(correctionNorm, std::numeric_limits<double>::epsilon());
CAPTURE(correctionNorm, repeatError);
const int locallyFiniteCorrections =
vector_is_finite(firstCorrection) && vector_is_finite(repeatedCorrection) ? 1 : 0;
int globallyFiniteCorrections = 0;
REQUIRE(MPI_Allreduce(
&locallyFiniteCorrections,
&globallyFiniteCorrections,
1,
MPI_INT,
MPI_MIN,
finiteElements.mesh->GetComm()
) == MPI_SUCCESS);
CHECK(globallyFiniteCorrections == 1);
CHECK(std::isfinite(correctionNorm));
CHECK(correctionNorm > 0.0);
CHECK(repeatError <= 2.0e-12);
require_all_ranks(
physicalInverse.IsCurrent() && scaledInverse.IsCurrent(),
communicator,
"preconditioner currentness after repeated application"
);
const mfem::Vector &readOnlyCorrection = firstCorrection;
const auto correctionView = problem.GetManifest().stateView(readOnlyCorrection);
const double massCorrection = correctionView.block(
utils::blocks::fixed_total_mass_constraint.mass_normalization_term
)(0);
const double angularVelocityCorrection = correctionView.block(
utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term
)(0);
const double phaseCorrection = correctionView.block(
utils::blocks::fixed_central_density_phase.central_value_term
)(0);
CAPTURE(massCorrection, angularVelocityCorrection, phaseCorrection);
check_rank_consistent_scalar(
massCorrection,
finiteElements.mesh->GetComm()
);
check_rank_consistent_scalar(
angularVelocityCorrection,
finiteElements.mesh->GetComm()
);
check_rank_consistent_scalar(
phaseCorrection,
finiteElements.mesh->GetComm()
);
/* A Newton iteration reparses the same normalized state under fresh
* dependency revisions. Every rank must observe the stale inverse, and
* refresh must reconstruct the inferred border actions and Schur data. */
const auto secondPreparation = normalized.Prepare(
normalizedState,
make_stellar_dependencies(5)
);
require_all_ranks(
secondPreparation.DidAnyWork() && normalized.IsPrepared(),
communicator,
"second variadic stellar-root preparation"
);
mfem::Vector secondPreparedResidual;
normalized.BuildResidual(secondPreparedResidual);
mfem::Vector secondPreparedResidualDifference(secondPreparedResidual);
secondPreparedResidualDifference -= normalizedResidual;
const double secondPreparedResidualError = global_norm(
secondPreparedResidualDifference,
finiteElements.mesh->GetComm()
) / std::max({
global_norm(secondPreparedResidual, finiteElements.mesh->GetComm()),
global_norm(normalizedResidual, finiteElements.mesh->GetComm()),
std::numeric_limits<double>::epsilon()
});
CAPTURE(secondPreparedResidualError);
CHECK(secondPreparedResidualError <= 2.0e-12);
require_all_ranks(
!physicalInverse.IsCurrent() && !scaledInverse.IsCurrent(),
communicator,
"preconditioners become stale together"
);
CHECK_THROWS_AS(
scaledInverse.Mult(normalizedRightHandSide, repeatedCorrection),
std::logic_error
);
const auto refresh = physicalInverse.Refresh();
CHECK(refresh.specificationActionsRefreshed);
CHECK(refresh.rebuiltSchurComplement);
CHECK(refresh.DidAnyWork());
require_all_ranks(
physicalInverse.IsCurrent() && scaledInverse.IsCurrent(),
communicator,
"refreshed preconditioner currentness"
);
const auto noOpRefresh = physicalInverse.Refresh();
CHECK_FALSE(noOpRefresh.DidAnyWork());
mfem::Vector refreshedCorrection(scaledInverse.Height());
refreshedCorrection = 0.0;
scaledInverse.Mult(normalizedRightHandSide, refreshedCorrection);
mfem::Vector refreshDifference(refreshedCorrection);
refreshDifference -= firstCorrection;
const double refreshError = global_norm(
refreshDifference,
finiteElements.mesh->GetComm()
) / std::max(
correctionNorm,
std::numeric_limits<double>::epsilon()
);
const int locallyFiniteRefresh = vector_is_finite(refreshedCorrection) ? 1 : 0;
int globallyFiniteRefresh = 0;
REQUIRE(MPI_Allreduce(
&locallyFiniteRefresh,
&globallyFiniteRefresh,
1,
MPI_INT,
MPI_MIN,
finiteElements.mesh->GetComm()
) == MPI_SUCCESS);
CAPTURE(refreshError);
CHECK(globallyFiniteRefresh == 1);
CHECK(refreshError <= 2.0e-10);
}
TEST_CASE( TEST_CASE(
"MPI Prepared Gravity Operators Preserve Global Algebraic Identities", "MPI Prepared Gravity Operators Preserve Global Algebraic Identities",
"[mpi][distributed][gravity][operators][unit]" "[mpi][distributed][gravity][operators][unit]"

View File

@@ -0,0 +1,828 @@
#include <algorithm>
#include <array>
#include <cmath>
#include <concepts>
#include <limits>
#include <span>
#include <stdexcept>
#include <utility>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
import mean_field;
import test_helpers;
namespace {
namespace blocks = mean_field::utils::blocks;
namespace normalization = mean_field::normalization;
namespace models = mean_field::models;
struct ModelWithoutFixedTotalMass final { };
template <typename Model>
concept SupportsModelDerivedStellarScales = requires(
const normalization::PhysicalRieszDiagonal<> &policy,
const Model &model
) {
{
normalization::deriveStellarCharacteristicScales(policy, model)
} -> std::same_as<normalization::StellarCharacteristicScales>;
};
struct TestValue final : blocks::value_block_base { };
struct TestResidual final : blocks::residual_block_base { };
using TestForm = blocks::block_form<
blocks::type_list<TestValue>,
blocks::type_list<TestResidual>>;
using GlobalSpecificEnergyNormalization = models::CoordinateNormalization<
models::RieszTopology::global_scalar,
models::PhysicalScaleLaw::specific_energy>;
using VolumeSpecificEnergyNormalization = models::CoordinateNormalization<
models::RieszTopology::scalar_volume_l2,
models::PhysicalScaleLaw::specific_energy>;
class SelfDescribingMagneticSpecificEnergy final {
public:
struct Parameters final {
double target;
};
using ModelDefinition = mean_field::integral::FixedWithPhysicalCoordinate<
SelfDescribingMagneticSpecificEnergy,
"NormalizationMockMagneticSpecificEnergy",
models::DependsOn<blocks::density::mass::value>,
models::Affects<blocks::enthalpy::specific::residual>,
models::GlobalScalarNormalization<
models::PhysicalScaleLaw::dimensionless,
models::PhysicalScaleLaw::specific_energy>>;
explicit constexpr SelfDescribingMagneticSpecificEnergy(const Parameters parameters) noexcept
: m_target(parameters.target) {
}
private:
double m_target;
};
class MissingGeneratedNormalization final {
public:
struct Parameters final {
double target;
};
using ModelDefinition = mean_field::integral::FixedWithMultiplier<
MissingGeneratedNormalization,
"NormalizationMockMissing",
models::DependsOn<blocks::density::mass::value>,
models::Affects<blocks::enthalpy::specific::residual>>;
explicit constexpr MissingGeneratedNormalization(const Parameters parameters) noexcept
: m_target(parameters.target) {
}
private:
double m_target;
};
class GeneratedVolumeCoordinateWithoutMetricSource final {
public:
struct Parameters final {
double target;
};
using ModelDefinition = mean_field::integral::FixedWithPhysicalCoordinate<
GeneratedVolumeCoordinateWithoutMetricSource,
"NormalizationMockVolumeCoordinate",
models::DependsOn<blocks::density::mass::value>,
models::Affects<blocks::enthalpy::specific::residual>,
models::GeneratedNormalization<
VolumeSpecificEnergyNormalization,
GlobalSpecificEnergyNormalization>>;
explicit constexpr GeneratedVolumeCoordinateWithoutMetricSource(const Parameters parameters) noexcept
: m_target(parameters.target) {
}
private:
double m_target;
};
struct MalformedGeneratedNormalization final { };
class MalformedGeneratedNormalizationConstraint final {
public:
struct Parameters final {
double target;
};
using ModelDefinition = mean_field::integral::FixedWithMultiplier<
MalformedGeneratedNormalizationConstraint,
"NormalizationMockMalformed",
models::DependsOn<blocks::density::mass::value>,
models::Affects<blocks::enthalpy::specific::residual>,
MalformedGeneratedNormalization>;
explicit constexpr MalformedGeneratedNormalizationConstraint(const Parameters parameters) noexcept
: m_target(parameters.target) {
}
private:
double m_target;
};
template <typename Specification>
using GeneratedValueBlock = blocks::generated_value_block<
models::PhysicalCoordinateFor<Specification>>;
template <typename Specification>
using GeneratedMultiplierBlock = blocks::generated_value_block<
models::MultiplierFor<Specification>>;
template <typename Specification>
using GeneratedResidualBlock = blocks::generated_residual_block<
models::ResidualFor<Specification>>;
using SelfDescribingValue = GeneratedValueBlock<SelfDescribingMagneticSpecificEnergy>;
using SelfDescribingResidual = GeneratedResidualBlock<SelfDescribingMagneticSpecificEnergy>;
using SelfDescribingForm = blocks::block_form<
blocks::type_list<SelfDescribingValue>,
blocks::type_list<SelfDescribingResidual>>;
using MissingValue = GeneratedMultiplierBlock<MissingGeneratedNormalization>;
using MissingResidual = GeneratedResidualBlock<MissingGeneratedNormalization>;
using MissingNormalizationForm = blocks::block_form<
blocks::type_list<MissingValue>,
blocks::type_list<MissingResidual>>;
using UnpreparedVolumeValue = GeneratedValueBlock<GeneratedVolumeCoordinateWithoutMetricSource>;
using UnpreparedVolumeResidual = GeneratedResidualBlock<GeneratedVolumeCoordinateWithoutMetricSource>;
using UnpreparedVolumeForm = blocks::block_form<
blocks::type_list<UnpreparedVolumeValue>,
blocks::type_list<UnpreparedVolumeResidual>>;
class DenseOperator final : public mfem::Operator {
public:
explicit DenseOperator(const mfem::DenseMatrix &matrix)
: mfem::Operator(matrix.Height(), matrix.Width()),
m_matrix(matrix) {
}
void Mult(
const mfem::Vector &input,
mfem::Vector &output
) const override {
m_matrix.Mult(input, output);
}
private:
mfem::DenseMatrix m_matrix;
};
class DenseInverseSolver final : public mfem::Solver {
public:
explicit DenseInverseSolver(const mfem::DenseMatrix &inverse)
: mfem::Solver(inverse.Height(), inverse.Width()),
m_inverse(inverse) {
}
void SetOperator(const mfem::Operator &operation) override {
if (operation.Height() != Height() || operation.Width() != Width()) {
throw std::invalid_argument("The dense inverse received an incompatible operator.");
}
m_boundOperator = &operation;
++m_bindings;
}
void Mult(
const mfem::Vector &input,
mfem::Vector &output
) const override {
if (m_boundOperator == nullptr) {
throw std::logic_error("The dense inverse must be bound before application.");
}
m_inverse.Mult(input, output);
}
[[nodiscard]] const mfem::Operator *BoundOperator() const noexcept {
return m_boundOperator;
}
[[nodiscard]] int Bindings() const noexcept {
return m_bindings;
}
private:
mfem::DenseMatrix m_inverse;
const mfem::Operator *m_boundOperator{nullptr};
int m_bindings{0};
};
[[nodiscard]] mfem::Vector vector(std::initializer_list<double> values) {
mfem::Vector result(static_cast<int>(values.size()));
int index = 0;
for (const double value : values) {
result(index++) = value;
}
return result;
}
void checkVector(
const mfem::Vector &actual,
const mfem::Vector &expected,
const double epsilon = 2.0e-13
) {
REQUIRE(actual.Size() == expected.Size());
for (int index = 0; index < actual.Size(); ++index) {
CHECK(actual(index) == Catch::Approx(expected(index)).epsilon(epsilon).margin(1.0e-300));
}
}
} // namespace
TEST_CASE(
"Pointer-Retaining Normalization Operators Reject Temporary Dependencies",
"[normalization][type][lifetime]"
) {
using Map = normalization::DiagonalNormalization;
STATIC_CHECK(std::constructible_from<
normalization::ScaledJacobianOperator,
const DenseOperator &,
const Map &>);
STATIC_CHECK_FALSE(std::constructible_from<
normalization::ScaledJacobianOperator,
DenseOperator &&,
const Map &>);
STATIC_CHECK_FALSE(std::constructible_from<
normalization::ScaledJacobianOperator,
const DenseOperator &&,
const Map &>);
STATIC_CHECK_FALSE(std::constructible_from<
normalization::ScaledJacobianOperator,
const DenseOperator &,
Map &&>);
STATIC_CHECK(std::constructible_from<
normalization::ScaledInverseOperator,
const DenseOperator &,
const Map &>);
STATIC_CHECK_FALSE(std::constructible_from<
normalization::ScaledInverseOperator,
DenseOperator &&,
const Map &>);
STATIC_CHECK_FALSE(std::constructible_from<
normalization::ScaledInverseOperator,
const DenseOperator &,
Map &&>);
STATIC_CHECK(std::constructible_from<
normalization::ScaledPreconditioner,
DenseInverseSolver &,
const DenseOperator &,
const DenseOperator &,
const Map &>);
STATIC_CHECK_FALSE(std::constructible_from<
normalization::ScaledPreconditioner,
DenseInverseSolver &&,
const DenseOperator &,
const DenseOperator &,
const Map &>);
STATIC_CHECK_FALSE(std::constructible_from<
normalization::ScaledPreconditioner,
DenseInverseSolver &,
DenseOperator &&,
const DenseOperator &,
const Map &>);
STATIC_CHECK_FALSE(std::constructible_from<
normalization::ScaledPreconditioner,
DenseInverseSolver &,
const DenseOperator &,
DenseOperator &&,
const Map &>);
STATIC_CHECK_FALSE(std::constructible_from<
normalization::ScaledPreconditioner,
DenseInverseSolver &,
const DenseOperator &,
const DenseOperator &,
Map &&>);
}
TEST_CASE("Characteristic Stellar Scales Satisfy Gravity Virial And Rotation Identities", "[normalization][physics]") {
using namespace mean_field;
constexpr double mass = 7.0;
constexpr double radius = 3.0;
constexpr double gravity = 5.0;
const auto scales = normalization::deriveStellarCharacteristicScales(
dimensions::MassValue{mass}, dimensions::LengthValue{radius}, gravity
);
CHECK(scales.density == Catch::Approx(mass / std::pow(radius, 3)));
CHECK(scales.acceleration == Catch::Approx(gravity * mass / std::pow(radius, 2)));
CHECK(scales.specificEnergy == Catch::Approx(gravity * mass / radius));
CHECK(scales.pressure == Catch::Approx(gravity * mass * mass / std::pow(radius, 4)));
CHECK(scales.angularVelocity == Catch::Approx(std::sqrt(gravity * mass / std::pow(radius, 3))));
CHECK(scales.angularMomentum == Catch::Approx(mass * std::sqrt(gravity * mass * radius)));
// Hydrostatic/virial energy scales agree: P R^3 = M Phi = F R.
const double virial = scales.pressure * std::pow(radius, 3);
CHECK(virial == Catch::Approx(mass * scales.specificEnergy).epsilon(2.0e-15));
CHECK(virial == Catch::Approx(scales.force * radius).epsilon(2.0e-15));
// Omega_0 is the Kepler/break-up scale and J_0 = M R^2 Omega_0.
CHECK(scales.angularVelocity * scales.angularVelocity * radius ==
Catch::Approx(scales.acceleration).epsilon(2.0e-15));
CHECK(scales.angularMomentum ==
Catch::Approx(mass * radius * radius * scales.angularVelocity).epsilon(2.0e-15));
}
TEST_CASE("Characteristic Scales Obey The Expected Stellar Homology Exponents", "[normalization][physics]") {
using namespace mean_field;
const auto reference = normalization::deriveStellarCharacteristicScales(
dimensions::MassValue{2.5}, dimensions::LengthValue{4.0}, 3.0
);
constexpr double massFactor = 11.0;
constexpr double radiusFactor = 0.2;
constexpr double gravityFactor = 7.0;
const auto transformed = normalization::deriveStellarCharacteristicScales(
dimensions::MassValue{2.5 * massFactor},
dimensions::LengthValue{4.0 * radiusFactor},
3.0 * gravityFactor
);
CHECK(transformed.density / reference.density ==
Catch::Approx(massFactor / std::pow(radiusFactor, 3)).epsilon(4.0e-15));
CHECK(transformed.acceleration / reference.acceleration ==
Catch::Approx(gravityFactor * massFactor / std::pow(radiusFactor, 2)).epsilon(4.0e-15));
CHECK(transformed.inverseTimeSquared / reference.inverseTimeSquared ==
Catch::Approx(gravityFactor * massFactor / std::pow(radiusFactor, 3)).epsilon(4.0e-15));
CHECK(transformed.specificEnergy / reference.specificEnergy ==
Catch::Approx(gravityFactor * massFactor / radiusFactor).epsilon(4.0e-15));
CHECK(transformed.pressure / reference.pressure ==
Catch::Approx(gravityFactor * massFactor * massFactor / std::pow(radiusFactor, 4)).epsilon(4.0e-15));
CHECK(transformed.angularVelocity / reference.angularVelocity == Catch::Approx(
std::sqrt(gravityFactor * massFactor / std::pow(radiusFactor, 3))
).epsilon(4.0e-15));
CHECK(transformed.angularMomentum / reference.angularMomentum == Catch::Approx(
massFactor * std::sqrt(gravityFactor * massFactor * radiusFactor)
).epsilon(4.0e-15));
}
TEST_CASE("Physical Block Scales Distinguish Invariants From Numerical Phase Conditions", "[normalization][physics]") {
using namespace mean_field;
const auto scales = normalization::deriveStellarCharacteristicScales(
dimensions::MassValue{9.0}, dimensions::LengthValue{2.0}, 4.0
);
CHECK(normalization::physicalScale<blocks::density::mass::value>(scales) == scales.density);
CHECK(normalization::physicalScale<blocks::gravity::gradient::value>(scales) == scales.acceleration);
CHECK(normalization::physicalScale<blocks::gravity::poisson::residual>(scales) == scales.inverseTimeSquared);
CHECK(normalization::physicalScale<blocks::fixed_total_mass::mass_normalization::residual>(scales) == 9.0);
CHECK(normalization::physicalScale<blocks::fixed_angular_momentum::angular_velocity::value>(scales) ==
scales.angularVelocity);
CHECK(normalization::physicalScale<blocks::fixed_angular_momentum::angular_velocity::residual>(scales) ==
scales.angularMomentum);
// The central-density condition is implemented as h(0)-h_target, so its residual scale is energy/mass,
// despite the physical target being expressed as a density.
CHECK(normalization::physicalScale<blocks::fixed_central_density::central_value::residual>(scales) ==
scales.specificEnergy);
CHECK(normalization::physicalScale<blocks::fixed_central_density::central_value::residual>(scales) !=
scales.density);
}
TEST_CASE(
"Generated Physical Riesz Laws Come From A Self-Describing Physics Specification",
"[normalization][type][extension]"
) {
using namespace mean_field;
using ValueTraits = normalization::PhysicalRieszBlockTraits<SelfDescribingValue>;
using ResidualTraits = normalization::PhysicalRieszBlockTraits<SelfDescribingResidual>;
STATIC_CHECK(models::SelfDescribingModelSpecification<SelfDescribingMagneticSpecificEnergy>);
STATIC_CHECK(models::CompleteGeneratedNormalizationFor<SelfDescribingMagneticSpecificEnergy>);
STATIC_CHECK(operators::StellarEquilibriumSpecificationCompilable<SelfDescribingMagneticSpecificEnergy>);
STATIC_CHECK(normalization::GeneratedValuePhysicalRieszNormalizable<
models::PhysicalCoordinateFor<SelfDescribingMagneticSpecificEnergy>>);
STATIC_CHECK(normalization::GeneratedResidualPhysicalRieszNormalizable<
models::ResidualFor<SelfDescribingMagneticSpecificEnergy>>);
STATIC_CHECK(normalization::CompleteGeneratedPhysicalRieszNormalizationFor<
SelfDescribingMagneticSpecificEnergy>);
STATIC_CHECK(normalization::CompilableNormalizationFor<
normalization::PhysicalRieszDiagonal<>,
SelfDescribingForm>);
STATIC_CHECK(normalization::RegisteredStellarSpecificationNormalization<
SelfDescribingMagneticSpecificEnergy>);
STATIC_CHECK(normalization::CompleteStellarSpecificationNormalizationFor<
SelfDescribingMagneticSpecificEnergy,
SelfDescribingForm>);
STATIC_CHECK(ValueTraits::Method::topology == normalization::RieszTopology::global_scalar);
STATIC_CHECK(ValueTraits::Method::scale == normalization::PhysicalScaleKind::dimensionless);
STATIC_CHECK(ResidualTraits::Method::topology == normalization::RieszTopology::global_scalar);
STATIC_CHECK(ResidualTraits::Method::scale == normalization::PhysicalScaleKind::specific_energy);
const auto scales = normalization::deriveStellarCharacteristicScales(
dimensions::MassValue{9.0}, dimensions::LengthValue{2.0}, 4.0
);
const blocks::form_layout<SelfDescribingForm> layout({1}, {1});
normalization::DiagonalNormalizationBuilder<SelfDescribingForm> builder(layout);
normalization::StellarSpecificationNormalizationContribution<
SelfDescribingMagneticSpecificEnergy>::Apply(builder, scales);
const normalization::DiagonalNormalization map = std::move(builder).Build();
REQUIRE(map.StateFactors().Size() == 1);
REQUIRE(map.ResidualFactors().Size() == 1);
CHECK(map.StateFactors()(0) == Catch::Approx(1.0).epsilon(2.0e-15));
CHECK(map.ResidualFactors()(0) == Catch::Approx(1.0 / scales.specificEnergy).epsilon(2.0e-15));
}
TEST_CASE(
"Generated Normalization Completeness Is SFINAE Safe And Rejects Missing Runtime Metrics",
"[normalization][type][validation]"
) {
using namespace mean_field;
STATIC_CHECK_FALSE(normalization::CompleteGeneratedPhysicalRieszNormalizationFor<int>);
STATIC_CHECK_FALSE(normalization::RegisteredStellarSpecificationNormalization<int>);
STATIC_CHECK_FALSE(normalization::CompleteStellarNormalizationFor<int, SelfDescribingForm>);
STATIC_CHECK(models::ModelSpecification<MissingGeneratedNormalization>);
STATIC_CHECK_FALSE(models::CompleteGeneratedNormalizationFor<MissingGeneratedNormalization>);
STATIC_CHECK_FALSE(normalization::CompleteGeneratedPhysicalRieszNormalizationFor<
MissingGeneratedNormalization>);
STATIC_CHECK_FALSE(normalization::CompilableNormalizationFor<
normalization::PhysicalRieszDiagonal<>,
MissingNormalizationForm>);
STATIC_CHECK_FALSE(normalization::RegisteredStellarSpecificationNormalization<
MissingGeneratedNormalization>);
STATIC_CHECK_FALSE(normalization::CompleteStellarSpecificationNormalizationFor<
MissingGeneratedNormalization,
MissingNormalizationForm>);
STATIC_CHECK(models::ModelSpecification<MalformedGeneratedNormalizationConstraint>);
STATIC_CHECK_FALSE(models::CompleteGeneratedNormalizationFor<
MalformedGeneratedNormalizationConstraint>);
STATIC_CHECK_FALSE(normalization::CompleteGeneratedPhysicalRieszNormalizationFor<
MalformedGeneratedNormalizationConstraint>);
STATIC_CHECK_FALSE(normalization::RegisteredStellarSpecificationNormalization<
MalformedGeneratedNormalizationConstraint>);
// The declaration itself is a valid Riesz law, but runtime stellar
// preparation has no finite-element Gram source for a generated volume
// field. The stronger runtime concept must therefore reject it.
STATIC_CHECK(normalization::CompleteGeneratedPhysicalRieszNormalizationFor<
GeneratedVolumeCoordinateWithoutMetricSource>);
STATIC_CHECK(normalization::CompilableNormalizationFor<
normalization::PhysicalRieszDiagonal<>,
UnpreparedVolumeForm>);
STATIC_CHECK_FALSE(normalization::RegisteredStellarSpecificationNormalization<
GeneratedVolumeCoordinateWithoutMetricSource>);
STATIC_CHECK_FALSE(normalization::CompleteStellarSpecificationNormalizationFor<
GeneratedVolumeCoordinateWithoutMetricSource,
UnpreparedVolumeForm>);
}
TEST_CASE("Characteristic Scale Construction Rejects Invalid Or Overflowing References", "[normalization][validation]") {
using namespace mean_field;
CHECK_THROWS_AS(
normalization::deriveStellarCharacteristicScales(
dimensions::MassValue{0.0}, dimensions::LengthValue{1.0}, 1.0
),
std::invalid_argument
);
CHECK_THROWS_AS(
normalization::deriveStellarCharacteristicScales(
dimensions::MassValue{1.0}, dimensions::LengthValue{-1.0}, 1.0
),
std::invalid_argument
);
CHECK_THROWS_AS(
(normalization::PhysicalRieszDiagonal{dimensions::LengthValue{1.0},
std::numeric_limits<double>::quiet_NaN()}),
std::invalid_argument
);
CHECK_THROWS_AS(
normalization::deriveStellarCharacteristicScales(
dimensions::MassValue{1.0e300}, dimensions::LengthValue{1.0e-200}, 1.0e100
),
std::overflow_error
);
}
TEST_CASE("Diagonal Riesz Maps Reproduce Primal And Dual Norms Across Extreme Metrics", "[normalization][math]") {
const blocks::form_layout<TestForm> layout({3}, {3});
normalization::DiagonalNormalizationBuilder<TestForm> builder(layout);
const mfem::Vector gram = vector({1.0e-20, 4.0, 9.0e20});
constexpr double stateScale = 10.0;
constexpr double residualScale = 0.25;
builder.SetValueBlock<TestValue>(stateScale, gram);
builder.SetResidualBlock<TestResidual>(residualScale, gram);
const normalization::DiagonalNormalization map = std::move(builder).Build();
const mfem::Vector state = vector({3.0e10, -2.0, 4.0e-10});
const mfem::Vector residual = vector({2.0e-10, -3.0, 5.0e10});
double expectedPrimalNormSquared = 0.0;
double expectedDualNormSquared = 0.0;
for (int index = 0; index < gram.Size(); ++index) {
expectedPrimalNormSquared += gram(index) * state(index) * state(index) /
(stateScale * stateScale);
expectedDualNormSquared += residual(index) * residual(index) /
(gram(index) * residualScale * residualScale);
}
CHECK(map.LocalStateNormSquared(state) == Catch::Approx(expectedPrimalNormSquared).epsilon(3.0e-15));
CHECK(map.LocalResidualNormSquared(residual) == Catch::Approx(expectedDualNormSquared).epsilon(3.0e-15));
mfem::Vector normalizedState;
mfem::Vector recoveredState;
mfem::Vector normalizedResidual;
mfem::Vector recoveredResidual;
map.NormalizeState(state, normalizedState);
map.DenormalizeState(normalizedState, recoveredState);
map.NormalizeResidual(residual, normalizedResidual);
map.DenormalizeResidual(normalizedResidual, recoveredResidual);
checkVector(recoveredState, state, 3.0e-15);
checkVector(recoveredResidual, residual, 3.0e-15);
}
TEST_CASE("Hybrid Riesz Rows Replace Missing Volume Metrics With Point Metrics", "[normalization][math]") {
using HybridForm = blocks::block_form<
blocks::type_list<blocks::enthalpy::specific::value>,
blocks::type_list<blocks::enthalpy::specific::residual>>;
const blocks::form_layout<HybridForm> layout({4}, {4});
normalization::DiagonalNormalizationBuilder<HybridForm> builder(layout);
builder.SetValueBlock<blocks::enthalpy::specific::value>(2.0, vector({2.0, 3.0, 5.0, 7.0}));
// Replaced isobaric rows may have zero bulk mass because they are no longer volume weak rows.
const mfem::Vector bulkMetric = vector({4.0, 0.0, 16.0, 0.0});
const std::array<int, 2> pointRows{1, 3};
builder.SetHybridResidualBlock<blocks::enthalpy::specific::residual>(
5.0, bulkMetric, std::span<const int>{pointRows}, 1.0
);
const auto map = std::move(builder).Build();
CHECK(map.ResidualFactors()(0) == Catch::Approx(1.0 / 10.0));
CHECK(map.ResidualFactors()(1) == Catch::Approx(1.0 / 5.0));
CHECK(map.ResidualFactors()(2) == Catch::Approx(1.0 / 20.0));
CHECK(map.ResidualFactors()(3) == Catch::Approx(1.0 / 5.0));
normalization::DiagonalNormalizationBuilder<HybridForm> duplicateRows(layout);
duplicateRows.SetValueGlobal<blocks::enthalpy::specific::value>(1.0);
const std::array<int, 2> duplicates{1, 1};
CHECK_THROWS_AS(
duplicateRows.SetHybridResidualBlock<blocks::enthalpy::specific::residual>(
1.0, vector({1.0, 1.0, 1.0, 1.0}), std::span<const int>{duplicates}
),
std::invalid_argument
);
}
TEST_CASE("Runtime Normalization Assembly Rejects Missing Duplicate And Invalid Data", "[normalization][validation]") {
const blocks::form_layout<TestForm> layout({2}, {2});
normalization::DiagonalNormalizationBuilder<TestForm> missing(layout);
missing.SetValueBlock<TestValue>(1.0, vector({1.0, 1.0}));
CHECK_THROWS_AS(std::move(missing).Build(), std::logic_error);
normalization::DiagonalNormalizationBuilder<TestForm> duplicate(layout);
duplicate.SetValueBlock<TestValue>(1.0, vector({1.0, 1.0}));
CHECK_THROWS_AS(duplicate.SetValueBlock<TestValue>(1.0, vector({1.0, 1.0})), std::logic_error);
normalization::DiagonalNormalizationBuilder<TestForm> zeroMetric(layout);
CHECK_THROWS_AS(zeroMetric.SetValueBlock<TestValue>(1.0, vector({1.0, 0.0})), std::invalid_argument);
normalization::DiagonalNormalizationBuilder<TestForm> wrongSize(layout);
CHECK_THROWS_AS(wrongSize.SetResidualBlock<TestResidual>(1.0, vector({1.0})), std::invalid_argument);
CHECK_THROWS_AS(
normalization::DiagonalNormalization(vector({1.0, std::numeric_limits<double>::infinity()}), vector({1.0})),
std::invalid_argument
);
}
TEST_CASE("Scaled Jacobian And Inverse Implement The Exact Coordinate Change", "[normalization][linear-algebra]") {
const mfem::Vector stateFactors = vector({1.0e-9, 2.0e7});
const mfem::Vector residualFactors = vector({5.0e8, 3.0e-6});
const normalization::DiagonalNormalization map(stateFactors, residualFactors);
// Start from a well-conditioned normalized Jacobian A_hat and form the dimensional
// J = L^{-1} A_hat R^{-1}. Its entries span the physical unit ranges, while L J R
// must recover A_hat rather than an artificially ill-conditioned dense matrix.
constexpr double normalizedMatrix[2][2]{{4.0, 1.0}, {2.0, 3.0}};
constexpr double normalizedInverse[2][2]{{0.3, -0.1}, {-0.2, 0.4}};
mfem::DenseMatrix matrix(2);
mfem::DenseMatrix inverse(2);
for (int row = 0; row < 2; ++row) {
for (int column = 0; column < 2; ++column) {
matrix(row, column) = normalizedMatrix[row][column] * stateFactors(column) /
residualFactors(row);
inverse(row, column) = normalizedInverse[row][column] * residualFactors(column) /
stateFactors(row);
}
}
const DenseOperator physicalJacobian(matrix);
const DenseOperator physicalInverse(inverse);
const normalization::ScaledJacobianOperator scaledJacobian(physicalJacobian, map);
const normalization::ScaledInverseOperator scaledInverse(physicalInverse, map);
const mfem::Vector direction = vector({0.75, -1.25});
mfem::Vector action;
scaledJacobian.Mult(direction, action);
mfem::Vector expected(2);
expected(0) = 4.0 * direction(0) + direction(1);
expected(1) = 2.0 * direction(0) + 3.0 * direction(1);
checkVector(action, expected, 4.0e-15);
mfem::Vector recovered;
scaledInverse.Mult(action, recovered);
checkVector(recovered, direction, 2.0e-13);
}
TEST_CASE("Scaled Preconditioning Routes An Exact Physical Inverse Through FGMRES", "[normalization][solver]") {
const mfem::Vector stateFactors = vector({1.0e-9, 2.0e7});
const mfem::Vector residualFactors = vector({5.0e8, 3.0e-6});
const normalization::DiagonalNormalization map(stateFactors, residualFactors);
constexpr double normalizedMatrix[2][2]{{4.0, 1.0}, {2.0, 3.0}};
constexpr double normalizedInverse[2][2]{{0.3, -0.1}, {-0.2, 0.4}};
mfem::DenseMatrix physicalMatrix(2);
mfem::DenseMatrix physicalInverseMatrix(2);
for (int row = 0; row < 2; ++row) {
for (int column = 0; column < 2; ++column) {
physicalMatrix(row, column) = normalizedMatrix[row][column] * stateFactors(column) /
residualFactors(row);
physicalInverseMatrix(row, column) = normalizedInverse[row][column] * residualFactors(column) /
stateFactors(row);
}
}
const DenseOperator physicalJacobian(physicalMatrix);
const normalization::ScaledJacobianOperator scaledJacobian(physicalJacobian, map);
DenseInverseSolver physicalInverse(physicalInverseMatrix);
normalization::ScaledPreconditioner scaledPreconditioner(
physicalInverse, physicalJacobian, scaledJacobian, map
);
CHECK(physicalInverse.BoundOperator() == &physicalJacobian);
CHECK(&scaledPreconditioner.GetPhysicalJacobian() == &physicalJacobian);
CHECK(&scaledPreconditioner.GetNormalizedJacobian() == &scaledJacobian);
const mfem::Vector rightHandSide = vector({1.5, -0.75});
mfem::Vector directCorrection(2);
scaledPreconditioner.Mult(rightHandSide, directCorrection);
const mfem::Vector expected = vector({0.525, -0.6});
checkVector(directCorrection, expected, 3.0e-13);
mfem::FGMRESSolver krylov(MPI_COMM_WORLD);
krylov.SetRelTol(1.0e-13);
krylov.SetAbsTol(1.0e-15);
krylov.SetMaxIter(4);
krylov.SetKDim(2);
krylov.SetPrintLevel(0);
krylov.SetPreconditioner(scaledPreconditioner);
krylov.SetOperator(scaledJacobian);
mfem::Vector solution(2);
solution = 0.0;
krylov.Mult(rightHandSide, solution);
CHECK(krylov.GetConverged());
CHECK(krylov.GetNumIterations() <= 1);
checkVector(solution, expected, 3.0e-13);
CHECK(physicalInverse.BoundOperator() == &physicalJacobian);
CHECK(physicalInverse.Bindings() >= 2);
CHECK(scaledPreconditioner.GetStatistics().operatorBindings >= 2);
CHECK(scaledPreconditioner.GetStatistics().applications >= 2);
mfem::IdentityOperator differentNormalizedJacobian(2);
CHECK_THROWS_AS(
scaledPreconditioner.SetOperator(differentNormalizedJacobian),
std::invalid_argument
);
mfem::IdentityOperator wrongSize(3);
CHECK_THROWS_AS(scaledPreconditioner.SetOperator(wrongSize), std::invalid_argument);
mfem::Vector wrongCorrection(1);
CHECK_THROWS_AS(scaledPreconditioner.Mult(rightHandSide, wrongCorrection), std::invalid_argument);
}
TEST_CASE("Physical Riesz Scaling Collapses A Forty-Eight-Decade Diagonal Imbalance", "[normalization][numerics]") {
const mfem::Vector stateFactors = vector({1.0e-12, 1.0, 1.0e12});
const mfem::Vector residualFactors = vector({1.0e12, 1.0, 1.0e-12});
const normalization::DiagonalNormalization map(stateFactors, residualFactors);
mfem::DenseMatrix physicalMatrix(3);
physicalMatrix = 0.0;
for (int index = 0; index < 3; ++index) {
physicalMatrix(index, index) = stateFactors(index) / residualFactors(index);
}
CHECK(physicalMatrix(2, 2) / physicalMatrix(0, 0) == Catch::Approx(1.0e48));
const DenseOperator physicalJacobian(physicalMatrix);
const normalization::ScaledJacobianOperator scaledJacobian(physicalJacobian, map);
const mfem::Vector direction = vector({-2.0, 3.5, 0.125});
mfem::Vector action;
scaledJacobian.Mult(direction, action);
checkVector(action, direction, 4.0e-15);
}
TEST_CASE("A Compiled Stellar Problem Prepares Reference Physical Riesz Coordinates", "[normalization][integration]") {
using namespace mean_field;
utils::Args args = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(finiteElements.okay());
constexpr double targetMass = 2.0;
constexpr double referenceRadius = 1.25;
constexpr double gravitationalConstant = 3.0;
const normalization::PhysicalRieszDiagonal policy{
dimensions::LengthValue{referenceRadius}, gravitationalConstant
};
const auto discretization = equilibrium::makeStellarDiscretization(finiteElements, policy);
auto problem = equilibrium::discretize(
model::StellarModel(
eos::Polytrope({.n = 3.0, .K = 0.25}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{targetMass}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{1.0}})
),
discretization
);
using ProblemType = std::remove_cvref_t<decltype(problem)>;
using Form = typename ProblemType::FormType;
using ModelType = std::remove_cvref_t<decltype(problem.GetStellarModel())>;
STATIC_CHECK(SupportsModelDerivedStellarScales<ModelType>);
STATIC_CHECK_FALSE(SupportsModelDerivedStellarScales<ModelWithoutFixedTotalMass>);
STATIC_CHECK(std::same_as<
typename ProblemType::NormalizationPrescriptionType,
std::remove_cvref_t<decltype(policy)>>);
CHECK(problem.GetNormalizationPrescription().referenceRadius() == dimensions::LengthValue{referenceRadius});
const normalization::DiagonalNormalization map = normalization::prepareNormalization(problem);
REQUIRE(map.StateSize() == problem.StateSize());
REQUIRE(map.ResidualSize() == problem.EquationSize());
for (int index = 0; index < map.StateSize(); ++index) {
CHECK(std::isfinite(map.StateFactors()(index)));
CHECK(map.StateFactors()(index) > 0.0);
}
for (int index = 0; index < map.ResidualSize(); ++index) {
CHECK(std::isfinite(map.ResidualFactors()(index)));
CHECK(map.ResidualFactors()(index) > 0.0);
}
const auto scales = normalization::deriveStellarCharacteristicScales(policy, problem.GetStellarModel());
const auto &layout = problem.GetManifest().layout();
constexpr int massValueBlock = blocks::type_index_v<
blocks::fixed_total_mass::mass_normalization::value,
typename Form::value_blocks>;
constexpr int massResidualBlock = blocks::type_index_v<
blocks::fixed_total_mass::mass_normalization::residual,
typename Form::residual_blocks>;
constexpr int phaseValueBlock = blocks::type_index_v<
blocks::fixed_central_density::central_value::value,
typename Form::value_blocks>;
constexpr int phaseResidualBlock = blocks::type_index_v<
blocks::fixed_central_density::central_value::residual,
typename Form::residual_blocks>;
constexpr int enthalpyResidualBlock = blocks::type_index_v<
blocks::enthalpy::specific::residual,
typename Form::residual_blocks>;
CHECK(map.StateFactors()(layout.value_offsets()[massValueBlock]) ==
Catch::Approx(1.0 / scales.specificEnergy).epsilon(2.0e-15));
CHECK(map.ResidualFactors()(layout.residual_offsets()[massResidualBlock]) ==
Catch::Approx(1.0 / targetMass).epsilon(2.0e-15));
CHECK(map.StateFactors()(layout.value_offsets()[phaseValueBlock]) ==
Catch::Approx(1.0 / scales.specificEnergy).epsilon(2.0e-15));
CHECK(map.ResidualFactors()(layout.residual_offsets()[phaseResidualBlock]) ==
Catch::Approx(1.0 / scales.specificEnergy).epsilon(2.0e-15));
const auto &surfaceRows = problem.GetPressureSurfaceRows().reduced_dofs();
REQUIRE(surfaceRows.Size() > 0);
for (const int row : surfaceRows) {
const int rootRow = layout.residual_offsets()[enthalpyResidualBlock] + row;
CHECK(map.ResidualFactors()(rootRow) ==
Catch::Approx(1.0 / scales.specificEnergy).epsilon(2.0e-15));
}
mfem::Vector physicalState(problem.StateSize());
for (int index = 0; index < physicalState.Size(); ++index) {
physicalState(index) = std::sin(0.37 * static_cast<double>(index + 1));
}
mfem::Vector normalizedState;
mfem::Vector recoveredState;
map.NormalizeState(physicalState, normalizedState);
map.DenormalizeState(normalizedState, recoveredState);
checkVector(recoveredState, physicalState, 4.0e-15);
const normalization::ScaledJacobianOperator scaledJacobian(problem.GetLinearizationOperator(), map);
CHECK(scaledJacobian.Width() == problem.StateSize());
CHECK(scaledJacobian.Height() == problem.EquationSize());
// The existing provisional structure preconditioner remains available for this distinct problem type.
const auto structureBlock = preconditioning::stellarStructureBlock(problem);
STATIC_CHECK(preconditioning::PreconditionerComponent<std::remove_cvref_t<decltype(structureBlock)>>);
}

View File

@@ -0,0 +1,228 @@
#include <concepts>
#include <type_traits>
#include <utility>
#include <catch2/catch_test_macros.hpp>
import mean_field;
namespace {
namespace blocks = mean_field::utils::blocks;
namespace normalization = mean_field::normalization;
using PhysicalForm = blocks::surface_deformed_stellar_equilibrium_form;
using PhaseForm = blocks::central_density_bordered_stellar_equilibrium_form;
using PhysicalPlan = normalization::PhysicalRieszNormalizationPlanFor<PhysicalForm>;
using PhasePlan = normalization::PhysicalRieszNormalizationPlanFor<PhaseForm>;
struct ValueA final : blocks::value_block_base { };
struct ValueB final : blocks::value_block_base { };
struct ResidualA final : blocks::residual_block_base { };
struct ResidualB final : blocks::residual_block_base { };
struct ForeignValue final : blocks::value_block_base { };
struct ForeignResidual final : blocks::residual_block_base { };
using SmallForm = blocks::block_form<
blocks::type_list<ValueA, ValueB>,
blocks::type_list<ResidualA, ResidualB>>;
using ValueAIdentity = normalization::CoordinateComponent<
normalization::CoordinateKind::value,
blocks::type_list<ValueA>,
normalization::IdentityCoordinate>;
using ValueBIdentity = normalization::CoordinateComponent<
normalization::CoordinateKind::value,
blocks::type_list<ValueB>,
normalization::IdentityCoordinate>;
using ResidualAIdentity = normalization::CoordinateComponent<
normalization::CoordinateKind::residual,
blocks::type_list<ResidualA>,
normalization::IdentityCoordinate>;
using ResidualBIdentity = normalization::CoordinateComponent<
normalization::CoordinateKind::residual,
blocks::type_list<ResidualB>,
normalization::IdentityCoordinate>;
using ForeignIdentity = normalization::CoordinateComponent<
normalization::CoordinateKind::value,
blocks::type_list<ForeignValue>,
normalization::IdentityCoordinate>;
using ForeignResidualIdentity = normalization::CoordinateComponent<
normalization::CoordinateKind::residual,
blocks::type_list<ForeignResidual>,
normalization::IdentityCoordinate>;
using CompleteSmallPlan = normalization::NormalizationPlan<
ValueAIdentity,
ValueBIdentity,
ResidualAIdentity,
ResidualBIdentity>;
using MissingSmallPlan = normalization::NormalizationPlan<
ValueAIdentity,
ResidualAIdentity,
ResidualBIdentity>;
using DuplicateSmallPlan = normalization::NormalizationPlan<
ValueAIdentity,
ValueAIdentity,
ValueBIdentity,
ResidualAIdentity,
ResidualBIdentity>;
using ForeignSmallPlan = normalization::NormalizationPlan<
ValueAIdentity,
ValueBIdentity,
ForeignIdentity,
ResidualAIdentity,
ResidualBIdentity,
ForeignResidualIdentity>;
struct MalformedComponent final {
using Blocks = blocks::type_list<ValueA>;
using Method = normalization::IdentityCoordinate;
};
struct IncoherentComponent final {
using Blocks = blocks::type_list<ValueA>;
using Method = normalization::IdentityCoordinate;
using ValueBlocks = blocks::type_list<ValueB>;
using ResidualBlocks = blocks::type_list<>;
static constexpr auto kind = normalization::CoordinateKind::value;
};
using WrongDensityTopology = normalization::CoordinateComponent<
normalization::CoordinateKind::value,
blocks::type_list<blocks::density::mass::value>,
normalization::PhysicalRieszCoordinate<
normalization::RieszTopology::vector_volume_l2,
normalization::PhysicalScaleKind::density>>;
struct FutureInvariantValue final : blocks::value_block_base { };
struct FutureInvariantResidual final : blocks::residual_block_base { };
using UnregisteredFutureForm = blocks::block_form<
blocks::type_list<FutureInvariantValue>,
blocks::type_list<FutureInvariantResidual>>;
using BaseModel = mean_field::model::StellarModel<mean_field::models::SpecificationSet<
mean_field::eos::Polytrope,
mean_field::surface::Isobaric,
mean_field::integral::FixedTotalMass>>;
using RieszPolicy = normalization::PhysicalRieszDiagonal<>;
using RieszDiscretization = mean_field::equilibrium::StellarDiscretizationFor<RieszPolicy>;
using BaselineProblem = mean_field::equilibrium::StellarEquilibriumProblem<BaseModel>;
using RieszProblem = mean_field::equilibrium::StellarEquilibriumProblem<BaseModel, RieszDiscretization>;
using AngularModel = mean_field::model::StellarModel<mean_field::models::SpecificationSet<
mean_field::eos::Polytrope,
mean_field::surface::Isobaric,
mean_field::integral::FixedTotalMass,
mean_field::integral::FixedAngularMomentum>>;
using AngularForm = mean_field::operators::CompiledStellarEquilibriumForm<AngularModel>;
template <typename Mapper>
concept CanMakeRieszDiscretization = requires(
mean_field::fem::FEM &finiteElements,
Mapper &&mapper,
RieszPolicy policy
) {
mean_field::equilibrium::makeStellarDiscretization(
finiteElements,
std::forward<Mapper>(mapper),
policy
);
};
} // namespace
TEST_CASE("Normalization Plans Prove Exact Ownership Of Every Compiled Coordinate", "[normalization][type]") {
STATIC_CHECK(normalization::NormalizationPlanType<PhysicalPlan>);
STATIC_CHECK(normalization::CompleteNormalizationFor<PhysicalPlan, PhysicalForm>);
STATIC_CHECK(normalization::CompleteNormalizationFor<PhasePlan, PhaseForm>);
STATIC_CHECK(normalization::CompilableNormalizationFor<normalization::Unnormalized, PhysicalForm>);
STATIC_CHECK(normalization::CompilableNormalizationFor<RieszPolicy, PhysicalForm>);
STATIC_CHECK(normalization::CompilableNormalizationFor<RieszPolicy, PhaseForm>);
STATIC_CHECK(normalization::CompilableNormalizationFor<RieszPolicy, AngularForm>);
STATIC_CHECK(normalization::StellarSpecificationNormalizationContribution<
mean_field::integral::FixedAngularMomentum>::registered);
STATIC_CHECK(normalization::CompleteNormalizationFor<CompleteSmallPlan, SmallForm>);
STATIC_CHECK_FALSE(normalization::CompleteNormalizationFor<MissingSmallPlan, SmallForm>);
STATIC_CHECK_FALSE(normalization::CompleteNormalizationFor<DuplicateSmallPlan, SmallForm>);
STATIC_CHECK_FALSE(normalization::CompleteNormalizationFor<ForeignSmallPlan, SmallForm>);
using Missing = normalization::NormalizationCoverage<SmallForm, MissingSmallPlan>;
using Duplicate = normalization::NormalizationCoverage<SmallForm, DuplicateSmallPlan>;
using Foreign = normalization::NormalizationCoverage<SmallForm, ForeignSmallPlan>;
STATIC_CHECK(Missing::MissingValueBlocks::size == 1);
STATIC_CHECK(blocks::contains_type_v<ValueB, typename Missing::MissingValueBlocks>);
STATIC_CHECK(Duplicate::RepeatedValueBlocks::size == 1);
STATIC_CHECK(blocks::contains_type_v<ValueA, typename Duplicate::RepeatedValueBlocks>);
STATIC_CHECK(Foreign::UnexpectedValueBlocks::size == 1);
STATIC_CHECK(Foreign::UnexpectedResidualBlocks::size == 1);
}
TEST_CASE("Physical Riesz Methods Reject Incompatible Or Unregistered Field Topologies", "[normalization][type]") {
STATIC_CHECK_FALSE(normalization::NormalizationComponent<MalformedComponent>);
STATIC_CHECK_FALSE(normalization::NormalizationComponent<IncoherentComponent>);
STATIC_CHECK_FALSE(normalization::NormalizationComponent<WrongDensityTopology>);
STATIC_CHECK_FALSE(normalization::CompilableNormalizationFor<RieszPolicy, UnregisteredFutureForm>);
STATIC_CHECK(normalization::CompilableNormalizationFor<normalization::Unnormalized, UnregisteredFutureForm>);
using Density = normalization::PhysicalRieszBlockTraits<blocks::density::mass::value>;
using Gravity = normalization::PhysicalRieszBlockTraits<blocks::gravity::gradient::value>;
using Surface = normalization::PhysicalRieszBlockTraits<blocks::surface_deformation::parameters::value>;
using EnthalpyResidual = normalization::PhysicalRieszBlockTraits<blocks::enthalpy::specific::residual>;
using MassResidual = normalization::PhysicalRieszBlockTraits<
blocks::fixed_total_mass::mass_normalization::residual>;
STATIC_CHECK(Density::Method::topology == normalization::RieszTopology::scalar_volume_l2);
STATIC_CHECK(Gravity::Method::topology == normalization::RieszTopology::vector_volume_l2);
STATIC_CHECK(Surface::Method::topology == normalization::RieszTopology::scalar_boundary_l2);
STATIC_CHECK(
EnthalpyResidual::Method::topology == normalization::RieszTopology::hybrid_scalar_volume_point_rows
);
STATIC_CHECK(MassResidual::Method::topology == normalization::RieszTopology::global_scalar);
STATIC_CHECK(MassResidual::Method::scale == normalization::PhysicalScaleKind::mass);
}
TEST_CASE("Normalization Is Part Of The Compile-Time Discretization And Problem Type", "[normalization][type]") {
STATIC_CHECK(mean_field::equilibrium::StellarDiscretizationType<RieszDiscretization>);
STATIC_CHECK_FALSE(std::same_as<RieszDiscretization, mean_field::equilibrium::StellarDiscretization>);
STATIC_CHECK_FALSE(std::same_as<RieszProblem, BaselineProblem>);
STATIC_CHECK(std::same_as<typename BaselineProblem::NormalizationPrescriptionType, normalization::Unnormalized>);
STATIC_CHECK(std::same_as<typename RieszProblem::NormalizationPrescriptionType, RieszPolicy>);
STATIC_CHECK(mean_field::equilibrium::DiscretizedStellarEquilibriumProblem<RieszProblem>);
STATIC_CHECK(std::constructible_from<
RieszDiscretization,
mean_field::fem::FEM &,
const mean_field::mapping::DomainMapper &,
RieszPolicy>);
STATIC_CHECK_FALSE(std::constructible_from<
RieszDiscretization,
mean_field::fem::FEM &,
mean_field::mapping::DomainMapper &&,
RieszPolicy>);
STATIC_CHECK_FALSE(std::constructible_from<
RieszDiscretization,
mean_field::fem::FEM &,
const mean_field::mapping::DomainMapper &&,
RieszPolicy>);
STATIC_CHECK(std::constructible_from<
mean_field::equilibrium::StellarDiscretization,
mean_field::fem::FEM &,
const mean_field::mapping::DomainMapper &>);
STATIC_CHECK_FALSE(std::constructible_from<
mean_field::equilibrium::StellarDiscretization,
mean_field::fem::FEM &,
mean_field::mapping::DomainMapper &&>);
STATIC_CHECK_FALSE(std::constructible_from<
mean_field::equilibrium::StellarDiscretization,
mean_field::fem::FEM &,
const mean_field::mapping::DomainMapper &&>);
STATIC_CHECK(CanMakeRieszDiscretization<
mean_field::mapping::DomainMapper &>);
STATIC_CHECK_FALSE(CanMakeRieszDiscretization<
mean_field::mapping::DomainMapper>);
STATIC_CHECK_FALSE(CanMakeRieszDiscretization<
const mean_field::mapping::DomainMapper>);
using SmallLayout = blocks::form_layout<SmallForm>;
using SmallBuilder = normalization::DiagonalNormalizationBuilder<SmallForm>;
STATIC_CHECK(std::constructible_from<SmallBuilder, const SmallLayout &>);
STATIC_CHECK_FALSE(std::constructible_from<SmallBuilder, SmallLayout &&>);
STATIC_CHECK_FALSE(std::constructible_from<SmallBuilder, const SmallLayout &&>);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,531 @@
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <limits>
#include <type_traits>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
import mean_field;
import test_helpers;
namespace angular_momentum_test_utils {
[[nodiscard]] mean_field::operators::AngularMomentumDependencies makeDependencies() {
return {
.discretization = {.identity = 15013, .revision = 3},
.density = {.identity = 15017, .revision = 5},
.displacement = {.identity = 15031, .revision = 7},
.rotation = {.identity = 15053, .revision = 11}
};
}
[[nodiscard]] mean_field::operators::context::gravity_field::GravityFieldRevisions makeGravityRevisions(
const mean_field::operators::AngularMomentumDependencies &dependencies,
const std::uint64_t gravityGradientRevision = 13,
const std::uint64_t gravityPotentialRevision = 17
) {
return {
.discretization = {.value = dependencies.discretization.revision},
.displacement = {.value = dependencies.displacement.revision},
.density = {.value = dependencies.density.revision},
.gravity_gradient = {.value = gravityGradientRevision},
.gravity_potential = {.value = gravityPotentialRevision}
};
}
void prepareGravityContext(
mean_field::operators::context::gravity_field::GravityFieldLinearizationContext &context,
const mean_field::fem::FEM &finiteElements,
const mfem::Vector &density,
const mfem::Vector &displacement,
const mean_field::operators::AngularMomentumDependencies &dependencies,
const std::uint64_t gravityGradientRevision = 13,
const std::uint64_t gravityPotentialRevision = 17
) {
mfem::Vector gravityGradient(finiteElements.gravityFluxFes->GetTrueVSize());
mfem::Vector gravityPotential(finiteElements.gravityPotentialFes->GetTrueVSize());
gravityGradient = 0.0;
gravityPotential = 0.0;
context.Prepare(
{.density = context.GetDensityMap().gather(density),
.displacement = context.GetDisplacementMap().gather(displacement),
.gravity_gradient = context.GetGravityGradientMap().gather(gravityGradient),
.gravity_potential = context.GetGravityPotentialMap().gather(gravityPotential)},
makeGravityRevisions(dependencies, gravityGradientRevision, gravityPotentialRevision)
);
}
[[nodiscard]] mfem::Vector projectDensity(
const mean_field::fem::FEM &finiteElements,
const double phase
) {
mfem::ParGridFunction field(finiteElements.densityFes.get());
mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) {
return 0.94 + 0.08 * std::sin(0.71 * position(0) + phase) +
0.05 * std::cos(0.63 * position(1) - phase) + 0.03 * position(2) * position(2);
});
field.ProjectCoefficient(coefficient);
mfem::Vector result;
field.GetTrueDofs(result);
return result;
}
[[nodiscard]] mfem::Vector projectDensityDirection(
const mean_field::fem::FEM &finiteElements,
const double phase
) {
mfem::ParGridFunction field(finiteElements.densityFes.get());
mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) {
return 0.17 * std::sin(0.83 * position(0) + phase) -
0.12 * std::cos(0.79 * position(1) - phase) + 0.06 * position(2);
});
field.ProjectCoefficient(coefficient);
mfem::Vector result;
field.GetTrueDofs(result);
return result;
}
[[nodiscard]] mfem::Vector projectConstantDensity(
const mean_field::fem::FEM &finiteElements,
const double value,
mfem::ParGridFunction *fieldOutput = nullptr
) {
mfem::ParGridFunction field(finiteElements.densityFes.get());
mfem::ConstantCoefficient coefficient(value);
field.ProjectCoefficient(coefficient);
if (fieldOutput != nullptr) {
*fieldOutput = field;
}
mfem::Vector result;
field.GetTrueDofs(result);
return result;
}
[[nodiscard]] mfem::Vector projectAffineDisplacement(
const mean_field::fem::FEM &finiteElements,
const double scale
) {
mfem::ParGridFunction field(finiteElements.displacementFes.get());
mfem::VectorFunctionCoefficient coefficient(
finiteElements.mesh->Dimension(),
[scale](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(position.Size());
for (int component = 0; component < position.Size(); ++component) {
value(component) = scale * position(component);
}
}
);
field.ProjectCoefficient(coefficient);
mfem::Vector result;
field.GetTrueDofs(result);
return result;
}
[[nodiscard]] mfem::Vector projectDisplacementDirection(
const mean_field::fem::FEM &finiteElements,
const double scale
) {
mfem::ParGridFunction field(finiteElements.displacementFes.get());
mfem::VectorFunctionCoefficient coefficient(
finiteElements.mesh->Dimension(),
[scale](const mfem::Vector &position, mfem::Vector &value) {
value.SetSize(3);
value(0) = scale * (0.07 * position(0) + 0.018 * position(1) * position(2));
value(1) = scale * (-0.05 * position(1) + 0.013 * position(0) * position(2));
value(2) = scale * (0.04 * position(2) - 0.011 * position(0) * position(1));
}
);
field.ProjectCoefficient(coefficient);
mfem::Vector result;
field.GetTrueDofs(result);
return result;
}
[[nodiscard]] double residual(const mean_field::operators::PreparedAngularMomentumOperator &operation) {
mfem::Vector value;
operation.BuildResidual(value);
REQUIRE(value.Size() == 1);
return value(0);
}
[[nodiscard]] double relativeError(const double actual, const double expected) {
return std::abs(actual - expected) /
std::max({std::abs(actual), std::abs(expected), 100.0 * std::numeric_limits<double>::epsilon()});
}
} // namespace angular_momentum_test_utils
TEST_CASE(
"Prepared Angular Momentum Satisfies Moment Scaling And The Parallel Axis Theorem",
"[fixed-angular-momentum][physics][analytic]"
) {
using namespace mean_field;
using Catch::Approx;
using Operator = operators::PreparedAngularMomentumOperator;
STATIC_CHECK_FALSE(std::is_copy_constructible_v<Operator>);
STATIC_CHECK_FALSE(std::is_move_constructible_v<Operator>);
utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
constexpr double densityValue = 1.37;
constexpr double angularVelocity = 0.73;
constexpr double targetAngularMomentum = 0.41;
mfem::ParGridFunction densityField(finiteElements.densityFes.get());
const mfem::Vector density = angular_momentum_test_utils::projectConstantDensity(
finiteElements,
densityValue,
&densityField
);
mfem::Vector displacement(finiteElements.displacementFes->GetTrueVSize());
displacement = 0.0;
finiteElements.displacement->SetFromTrueDofs(displacement);
auto dependencies = angular_momentum_test_utils::makeDependencies();
operators::context::gravity_field::GravityFieldLinearizationContext gravityContext(
finiteElements,
*finiteElements.domainMapperStateless
);
angular_momentum_test_utils::prepareGravityContext(
gravityContext,
finiteElements,
density,
displacement,
dependencies
);
const models::CompiledFixedAngularMomentum originConstraint = models::compileConstraint(
integral::FixedAngularMomentum({
.Jtotal = dimensions::AngularMomentumValue{targetAngularMomentum},
.axis = {0.0, 0.0, 4.0}
})
);
Operator origin(
finiteElements,
*finiteElements.domainMapperStateless,
gravityContext,
originConstraint
);
const auto initial = origin.Prepare(angularVelocity, dependencies);
CHECK(initial.rebuiltStaticPlan);
CHECK(initial.refreshedGeometry);
CHECK(initial.refreshedDensity);
CHECK(initial.updatedAngularVelocity);
CHECK(initial.assembledResidual);
const double independentMoment = analysis::get_moment_of_inertia(finiteElements, densityField);
CHECK(angular_momentum_test_utils::relativeError(origin.GetMomentOfInertia(), independentMoment) < 2.0e-13);
CHECK(origin.GetCurrentAngularMomentum() ==
Approx(angularVelocity * origin.GetMomentOfInertia()).epsilon(2.0e-15));
CHECK(angular_momentum_test_utils::residual(origin) ==
Approx(angularVelocity * origin.GetMomentOfInertia() - targetAngularMomentum).epsilon(2.0e-15));
const auto report = origin.GetConstraintReport();
CHECK(report.targetAngularMomentum == targetAngularMomentum);
CHECK(report.achievedAngularMomentum == origin.GetCurrentAngularMomentum());
CHECK(report.momentOfInertia == origin.GetMomentOfInertia());
CHECK(report.angularVelocity == angularVelocity);
const physics::RigidRotation rotation = origin.GetRotation();
CHECK(rotation.angular_velocity()(0) == 0.0);
CHECK(rotation.angular_velocity()(1) == 0.0);
CHECK(rotation.angular_velocity()(2) == angularVelocity);
constexpr double affineScale = 0.086;
const mfem::Vector affineDisplacement =
angular_momentum_test_utils::projectAffineDisplacement(finiteElements, affineScale);
++dependencies.displacement.revision;
angular_momentum_test_utils::prepareGravityContext(
gravityContext,
finiteElements,
density,
affineDisplacement,
dependencies
);
const auto affine = origin.Prepare(angularVelocity, dependencies);
CHECK(affine.refreshedGeometry);
CHECK_FALSE(affine.refreshedDensity);
const double expectedAffineRatio = std::pow(1.0 + affineScale, 5);
const double measuredAffineRatio = origin.GetMomentOfInertia() / independentMoment;
INFO("Expected homothetic I ratio = " << expectedAffineRatio);
INFO("Measured homothetic I ratio = " << measuredAffineRatio);
CHECK(angular_momentum_test_utils::relativeError(measuredAffineRatio, expectedAffineRatio) < 7.0e-7);
displacement = 0.0;
++dependencies.displacement.revision;
angular_momentum_test_utils::prepareGravityContext(
gravityContext,
finiteElements,
density,
displacement,
dependencies
);
origin.Prepare(angularVelocity, dependencies);
constexpr std::array<double, 3> shiftedCenter{0.27, -0.19, 0.31};
Operator shifted(
finiteElements,
*finiteElements.domainMapperStateless,
gravityContext,
models::compileConstraint(integral::FixedAngularMomentum({
.Jtotal = dimensions::AngularMomentumValue{targetAngularMomentum},
.axis = {0.0, 0.0, 1.0},
.center = shiftedCenter
}))
);
shifted.Prepare(angularVelocity, dependencies);
const double mass = analysis::domain_integrate_grid_function(
finiteElements,
densityField,
utils::DOMAINS::STELLAR,
mapping::COORDINATE_SPACE::PHYSICAL
);
const mfem::Vector centerOfMass = analysis::get_com(finiteElements, densityField);
const double expectedShiftedMoment = origin.GetMomentOfInertia() +
mass * (shiftedCenter[0] * shiftedCenter[0] +
shiftedCenter[1] * shiftedCenter[1]) -
2.0 * mass * (shiftedCenter[0] * centerOfMass(0) +
shiftedCenter[1] * centerOfMass(1));
CHECK(angular_momentum_test_utils::relativeError(shifted.GetMomentOfInertia(), expectedShiftedMoment) < 3.0e-13);
}
TEST_CASE(
"Prepared Angular Momentum Jacobian Matches Density Geometry And Angular Velocity Differences",
"[fixed-angular-momentum][jacobian][accuracy]"
) {
using namespace mean_field;
utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
const mfem::Vector density = angular_momentum_test_utils::projectDensity(finiteElements, 0.31);
const mfem::Vector densityDirection =
angular_momentum_test_utils::projectDensityDirection(finiteElements, 0.67);
const mfem::Vector displacement =
angular_momentum_test_utils::projectDisplacementDirection(finiteElements, 0.43);
const mfem::Vector displacementDirection =
angular_momentum_test_utils::projectDisplacementDirection(finiteElements, -0.79);
constexpr double angularVelocity = 0.63;
constexpr double angularVelocityDirection = -0.37;
auto dependencies = angular_momentum_test_utils::makeDependencies();
operators::context::gravity_field::GravityFieldLinearizationContext gravityContext(
finiteElements,
*finiteElements.domainMapperStateless
);
angular_momentum_test_utils::prepareGravityContext(
gravityContext,
finiteElements,
density,
displacement,
dependencies
);
operators::PreparedAngularMomentumOperator operation(
finiteElements,
*finiteElements.domainMapperStateless,
gravityContext,
models::compileConstraint(
integral::FixedAngularMomentum({.Jtotal = dimensions::AngularMomentumValue{0.81}})
)
);
operation.Prepare(angularVelocity, dependencies);
const mfem::Vector reducedDensityDirection = gravityContext.GetDensityMap().gather(densityDirection);
const mfem::Vector reducedDisplacementDirection =
gravityContext.GetDisplacementMap().gather(displacementDirection);
mfem::Vector densityAction;
mfem::Vector geometryAction;
mfem::Vector angularVelocityAction;
mfem::Vector completeAction;
operation.ApplyDensityJacobianAction(reducedDensityDirection, densityAction);
operation.ApplyDisplacementJacobianAction(reducedDisplacementDirection, geometryAction);
operation.ApplyAngularVelocityJacobianAction(angularVelocityDirection, angularVelocityAction);
operation.ApplyCompleteJacobianAction(
reducedDensityDirection,
reducedDisplacementDirection,
angularVelocityDirection,
completeAction
);
CHECK(angular_momentum_test_utils::relativeError(
completeAction(0),
densityAction(0) + geometryAction(0) + angularVelocityAction(0)
) < 3.0e-15);
CHECK(angularVelocityAction(0) ==
Catch::Approx(operation.GetMomentOfInertia() * angularVelocityDirection).epsilon(2.0e-15));
constexpr double angularStep = 1.0e-6;
++dependencies.rotation.revision;
operation.Prepare(angularVelocity + angularStep * angularVelocityDirection, dependencies);
const double angularPlus = angular_momentum_test_utils::residual(operation);
++dependencies.rotation.revision;
operation.Prepare(angularVelocity - angularStep * angularVelocityDirection, dependencies);
const double angularMinus = angular_momentum_test_utils::residual(operation);
const double angularDifference = (angularPlus - angularMinus) / (2.0 * angularStep);
CHECK(angular_momentum_test_utils::relativeError(angularVelocityAction(0), angularDifference) < 2.0e-10);
constexpr double densityStep = 1.0e-3;
mfem::Vector densityPlus(density);
densityPlus.Add(densityStep, densityDirection);
++dependencies.density.revision;
angular_momentum_test_utils::prepareGravityContext(
gravityContext,
finiteElements,
densityPlus,
displacement,
dependencies
);
operation.Prepare(angularVelocity, dependencies);
const double densityPlusResidual = angular_momentum_test_utils::residual(operation);
mfem::Vector densityMinus(density);
densityMinus.Add(-densityStep, densityDirection);
++dependencies.density.revision;
angular_momentum_test_utils::prepareGravityContext(
gravityContext,
finiteElements,
densityMinus,
displacement,
dependencies
);
operation.Prepare(angularVelocity, dependencies);
const double densityMinusResidual = angular_momentum_test_utils::residual(operation);
const double densityDifference = (densityPlusResidual - densityMinusResidual) / (2.0 * densityStep);
CHECK(angular_momentum_test_utils::relativeError(densityAction(0), densityDifference) < 4.0e-8);
constexpr double geometryStep = 1.0e-6;
mfem::Vector displacementPlus(displacement);
displacementPlus.Add(geometryStep, displacementDirection);
++dependencies.density.revision;
++dependencies.displacement.revision;
angular_momentum_test_utils::prepareGravityContext(
gravityContext,
finiteElements,
density,
displacementPlus,
dependencies
);
operation.Prepare(angularVelocity, dependencies);
const double geometryPlusResidual = angular_momentum_test_utils::residual(operation);
mfem::Vector displacementMinus(displacement);
displacementMinus.Add(-geometryStep, displacementDirection);
++dependencies.displacement.revision;
angular_momentum_test_utils::prepareGravityContext(
gravityContext,
finiteElements,
density,
displacementMinus,
dependencies
);
operation.Prepare(angularVelocity, dependencies);
const double geometryMinusResidual = angular_momentum_test_utils::residual(operation);
const double geometryDifference = (geometryPlusResidual - geometryMinusResidual) / (2.0 * geometryStep);
INFO("Density angular-momentum derivative error = " <<
angular_momentum_test_utils::relativeError(densityAction(0), densityDifference));
INFO("Geometry angular-momentum derivative error = " <<
angular_momentum_test_utils::relativeError(geometryAction(0), geometryDifference));
CHECK(angular_momentum_test_utils::relativeError(geometryAction(0), geometryDifference) < 4.0e-7);
}
TEST_CASE(
"Prepared Angular Momentum Refreshes Only Changed Runtime Data",
"[fixed-angular-momentum][prepared][lifecycle]"
) {
using namespace mean_field;
utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
mfem::Vector density = angular_momentum_test_utils::projectDensity(finiteElements, 0.29);
mfem::Vector displacement =
angular_momentum_test_utils::projectDisplacementDirection(finiteElements, 0.41);
auto dependencies = angular_momentum_test_utils::makeDependencies();
std::uint64_t gravityPotentialRevision = 17;
operators::context::gravity_field::GravityFieldLinearizationContext gravityContext(
finiteElements,
*finiteElements.domainMapperStateless
);
angular_momentum_test_utils::prepareGravityContext(
gravityContext,
finiteElements,
density,
displacement,
dependencies,
13,
gravityPotentialRevision
);
operators::PreparedAngularMomentumOperator operation(
finiteElements,
*finiteElements.domainMapperStateless,
gravityContext,
models::compileConstraint(
integral::FixedAngularMomentum({.Jtotal = dimensions::AngularMomentumValue{0.71}})
)
);
operation.Prepare(0.52, dependencies);
const auto preparationCount = operation.GetPreparationCount();
const double moment = operation.GetMomentOfInertia();
const auto repeated = operation.Prepare(0.52, dependencies);
CHECK_FALSE(repeated.DidAnyWork());
CHECK(operation.GetPreparationCount() == preparationCount);
++gravityPotentialRevision;
angular_momentum_test_utils::prepareGravityContext(
gravityContext,
finiteElements,
density,
displacement,
dependencies,
13,
gravityPotentialRevision
);
const auto unrelatedPotential = operation.Prepare(0.52, dependencies);
CHECK_FALSE(unrelatedPotential.DidAnyWork());
const double residualBeforeRotation = angular_momentum_test_utils::residual(operation);
++dependencies.rotation.revision;
const auto rotationOnly = operation.Prepare(0.81, dependencies);
CHECK(rotationOnly.updatedAngularVelocity);
CHECK(rotationOnly.assembledResidual);
CHECK_FALSE(rotationOnly.refreshedDensity);
CHECK_FALSE(rotationOnly.refreshedGeometry);
CHECK(operation.GetMomentOfInertia() == moment);
CHECK(angular_momentum_test_utils::residual(operation) - residualBeforeRotation ==
Catch::Approx((0.81 - 0.52) * moment).epsilon(3.0e-15));
density = angular_momentum_test_utils::projectDensity(finiteElements, 0.83);
++dependencies.density.revision;
angular_momentum_test_utils::prepareGravityContext(
gravityContext,
finiteElements,
density,
displacement,
dependencies,
13,
gravityPotentialRevision
);
const auto densityOnly = operation.Prepare(0.81, dependencies);
CHECK(densityOnly.refreshedDensity);
CHECK_FALSE(densityOnly.refreshedGeometry);
CHECK_FALSE(densityOnly.updatedAngularVelocity);
displacement = angular_momentum_test_utils::projectDisplacementDirection(finiteElements, 0.87);
++dependencies.displacement.revision;
angular_momentum_test_utils::prepareGravityContext(
gravityContext,
finiteElements,
density,
displacement,
dependencies,
13,
gravityPotentialRevision
);
const auto geometryOnly = operation.Prepare(0.81, dependencies);
CHECK(geometryOnly.refreshedGeometry);
CHECK_FALSE(geometryOnly.refreshedDensity);
CHECK_FALSE(geometryOnly.updatedAngularVelocity);
}

View File

@@ -44,20 +44,11 @@ namespace {
} // namespace } // namespace
TEST_CASE( TEST_CASE(
"Central Density Bordered Root Preserves The Physical Operator Prefix", "Central Density Contribution Composes Through The Variadic Root",
tags::central_density_phase_integration tags::central_density_phase_integration
) { ) {
using namespace mean_field; using namespace mean_field;
STATIC_CHECK_FALSE(
std::same_as<
operators::PreparedStellarEquilibriumOperator, operators::PreparedCentralDensityStellarEquilibriumOperator>
);
STATIC_CHECK(
operators::CentralDensityStellarEquilibriumSpecificationModel::compilationClass ==
models::ModelCompilationClass::isolated_root
);
utils::Args args = test_utils::setup_args(); utils::Args args = test_utils::setup_args();
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.okay()); REQUIRE(f.okay());
@@ -80,7 +71,8 @@ TEST_CASE(
STATIC_CHECK( STATIC_CHECK(
std::same_as< std::same_as<
typename std::remove_cvref_t<decltype(equilibriumProblem)>::PreparedOperatorType, typename std::remove_cvref_t<decltype(equilibriumProblem)>::PreparedOperatorType,
operators::PreparedCentralDensityStellarEquilibriumOperator> operators::PreparedVariadicStellarEquilibriumOperator<
typename std::remove_cvref_t<decltype(equilibriumProblem)>::ModelType>>
); );
CHECK( CHECK(
equilibriumProblem.GetStellarModel().specification<constraint::FixedCentralDensity>().targetDensity() == equilibriumProblem.GetStellarModel().specification<constraint::FixedCentralDensity>().targetDensity() ==
@@ -95,19 +87,21 @@ TEST_CASE(
CHECK(borderedOperator.GetRootManifest().constraints().size() == 3); CHECK(borderedOperator.GetRootManifest().constraints().size() == 3);
CHECK(borderedOperator.GetRootManifest().specificationDescriptors().size() == 4); CHECK(borderedOperator.GetRootManifest().specificationDescriptors().size() == 4);
const auto constraints = borderedOperator.GetRootManifest().constraints(); const auto &centralDescriptor =
CHECK(constraints[2].stableId == "FixedCentralDensity"); borderedOperator.GetRootManifest().specification<constraint::FixedCentralDensity>();
CHECK(constraints[2].role == models::SpecificationRole::phase_condition); CHECK(centralDescriptor.stableId == "FixedCentralDensity");
CHECK(constraints[2].columnPolicy == operators::RootColumnPolicy::solver_border); CHECK(centralDescriptor.role == models::SpecificationRole::phase_condition);
CHECK(constraints[2].target == 1.0); CHECK(centralDescriptor.columnPolicy == operators::RootColumnPolicy::solver_border);
REQUIRE(constraints[2].carrierTarget.has_value()); CHECK(centralDescriptor.target == 1.0);
CHECK(*constraints[2].carrierTarget == 1.0); REQUIRE(centralDescriptor.carrierTarget.has_value());
CHECK(constraints[2].targetUnits == "density"); CHECK(*centralDescriptor.carrierTarget == 1.0);
CHECK(constraints[2].residualUnits == "specific_enthalpy"); CHECK(centralDescriptor.targetUnits == "density");
CHECK(centralDescriptor.residualUnits == "specific_enthalpy");
mfem::Vector physicalState(physicalOperator.Width()); mfem::Vector physicalState(physicalOperator.Width());
physicalState = 0.0; physicalState = 0.0;
const auto physicalStateView = physicalOperator.GetRootStateView(physicalState); const auto physicalStateView =
physicalOperator.GetRootManifest().stateView(physicalState);
physicalStateView.block(utils::blocks::density_field.mass_term) = 1.0; physicalStateView.block(utils::blocks::density_field.mass_term) = 1.0;
physicalStateView.block(utils::blocks::enthalpy_field.specific_term) = 1.0; physicalStateView.block(utils::blocks::enthalpy_field.specific_term) = 1.0;
@@ -118,10 +112,9 @@ TEST_CASE(
const operators::StellarEquilibriumDependencies dependencies = make_dependencies(); const operators::StellarEquilibriumDependencies dependencies = make_dependencies();
const physics::RigidRotation rotation = make_zero_rotation(); const physics::RigidRotation rotation = make_zero_rotation();
physicalOperator.Prepare(physicalState, dependencies, rotation); physicalOperator.Prepare(physicalState, dependencies, rotation);
const operators::PreparedCentralDensityStellarEquilibriumReport initialReport = const auto initialReport = equilibriumProblem.Prepare(borderedState, dependencies, rotation);
equilibriumProblem.Prepare(borderedState, dependencies, rotation);
CHECK(initialReport.physical.assembledResidual); CHECK(initialReport.physical.assembledResidual);
CHECK(initialReport.phase.assembledResidual); CHECK(initialReport.specification<constraint::FixedCentralDensity>().constraint.assembledResidual);
CHECK(initialReport.assembledResidual); CHECK(initialReport.assembledResidual);
mfem::Vector physicalResidual; mfem::Vector physicalResidual;
@@ -164,13 +157,13 @@ TEST_CASE(
const auto repeatedReport = borderedOperator.Prepare(borderedState, dependencies, rotation); const auto repeatedReport = borderedOperator.Prepare(borderedState, dependencies, rotation);
CHECK_FALSE(repeatedReport.physical.DidAnyWork()); CHECK_FALSE(repeatedReport.physical.DidAnyWork());
CHECK_FALSE(repeatedReport.phase.DidAnyWork()); CHECK_FALSE(repeatedReport.specification<constraint::FixedCentralDensity>().DidAnyWork());
CHECK_FALSE(repeatedReport.assembledResidual); CHECK(repeatedReport.assembledResidual);
borderedState(borderedState.Size() - 1) = 0.375; borderedState(borderedState.Size() - 1) = 0.375;
const auto borderReport = borderedOperator.Prepare(borderedState, dependencies, rotation); const auto borderReport = borderedOperator.Prepare(borderedState, dependencies, rotation);
CHECK_FALSE(borderReport.physical.DidAnyWork()); CHECK_FALSE(borderReport.physical.DidAnyWork());
CHECK(borderReport.phase.refreshedBorder); CHECK(borderReport.specification<constraint::FixedCentralDensity>().constraint.refreshedBorder);
CHECK(borderReport.assembledResidual); CHECK(borderReport.assembledResidual);
mfem::Vector borderOnlyDirection(borderedOperator.Width()); mfem::Vector borderOnlyDirection(borderedOperator.Width());

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <concepts> #include <concepts>
#include <numbers>
#include <type_traits> #include <type_traits>
#include <utility> #include <utility>
@@ -10,6 +11,83 @@
import mean_field; import mean_field;
import test_helpers; import test_helpers;
namespace outer_manifest_report_test {
template <mean_field::model::StellarModelType Model>
class PreparedEarlierMultiplier;
class EarlierMultiplier final {
public:
struct Parameters final {
mean_field::dimensions::SpecificEnergyValue target;
};
using ModelDefinition = mean_field::integral::FixedWithMultiplier<
EarlierMultiplier,
"AardvarkOuterManifestMultiplier",
mean_field::models::DependsOn<mean_field::models::stellar::state::Density>,
mean_field::models::Affects<mean_field::models::stellar::equation::HydrostaticBalance>,
mean_field::models::GlobalScalarNormalization<
mean_field::models::PhysicalScaleLaw::specific_energy,
mean_field::models::PhysicalScaleLaw::specific_energy>,
mean_field::models::GeneratedManifest<
"aardvark_outer_manifest.value",
"a",
"aardvark_outer_manifest.residual",
"R_a",
"specific_energy",
"specific_energy">>;
using EquilibriumPhysics =
mean_field::operators::SpecificationEquilibriumPhysics<
PreparedEarlierMultiplier>;
explicit EarlierMultiplier(const Parameters parameters) noexcept
: m_target(parameters.target) {
}
[[nodiscard]] mean_field::dimensions::SpecificEnergyValue target() const noexcept {
return m_target;
}
private:
mean_field::dimensions::SpecificEnergyValue m_target;
};
template <mean_field::model::StellarModelType Model>
class PreparedEarlierMultiplier final {
public:
using Report = mean_field::operators::EmptySpecificationPreparationReport;
explicit PreparedEarlierMultiplier(const EarlierMultiplier &) noexcept {
}
template <typename StateView>
[[nodiscard]] Report PrepareAfterPhysical(const StateView &) noexcept {
return {};
}
template <typename Equation, typename Row>
[[nodiscard]] mean_field::stellar::StructuralZero AddResidual(
Equation,
Row &
) const noexcept {
return mean_field::stellar::structuralZero;
}
template <typename Equation, typename State, typename Direction, typename Row>
[[nodiscard]] mean_field::stellar::StructuralZero AddJacobianAction(
mean_field::stellar::Derivative<Equation, State>,
const Direction &,
Row &
) const noexcept {
return mean_field::stellar::zeroDerivative;
}
[[nodiscard]] bool IsPrepared() const noexcept {
return true;
}
};
} // namespace outer_manifest_report_test
namespace { namespace {
using BaseModel = mean_field::model::StellarModel<mean_field::models::SpecificationSet< using BaseModel = mean_field::model::StellarModel<mean_field::models::SpecificationSet<
mean_field::eos::Polytrope, mean_field::eos::Polytrope,
@@ -22,9 +100,30 @@ namespace {
mean_field::integral::FixedTotalMass, mean_field::integral::FixedTotalMass,
mean_field::constraint::FixedCentralDensity>>; mean_field::constraint::FixedCentralDensity>>;
using AngularMomentumModel = mean_field::model::StellarModel<mean_field::models::SpecificationSet<
mean_field::eos::Polytrope,
mean_field::surface::Isobaric,
mean_field::integral::FixedTotalMass,
mean_field::integral::FixedAngularMomentum>>;
using AngularMomentumCentralDensityModel =
mean_field::model::StellarModel<mean_field::models::SpecificationSet<
mean_field::eos::Polytrope,
mean_field::surface::Isobaric,
mean_field::integral::FixedTotalMass,
mean_field::integral::FixedAngularMomentum,
mean_field::constraint::FixedCentralDensity>>;
using IncompleteModel = using IncompleteModel =
mean_field::model::StellarModel<mean_field::models::SpecificationSet<mean_field::eos::Polytrope>>; mean_field::model::StellarModel<mean_field::models::SpecificationSet<mean_field::eos::Polytrope>>;
using EarlierMultiplierModel =
mean_field::model::StellarModel<mean_field::models::SpecificationSet<
mean_field::eos::Polytrope,
mean_field::surface::Isobaric,
outer_manifest_report_test::EarlierMultiplier,
mean_field::integral::FixedTotalMass>>;
template <typename Candidate> template <typename Candidate>
concept HasLegacyNumericalModelAdapter = requires { typename Candidate::NumericalModelAdapter; }; concept HasLegacyNumericalModelAdapter = requires { typename Candidate::NumericalModelAdapter; };
@@ -58,6 +157,7 @@ namespace {
difference -= right; difference -= right;
return difference.Norml2() / std::max({1.0, left.Norml2(), right.Norml2()}); return difference.Norml2() / std::max({1.0, left.Norml2(), right.Norml2()});
} }
} // namespace } // namespace
TEST_CASE( TEST_CASE(
@@ -68,26 +168,65 @@ TEST_CASE(
using BaseProblem = equilibrium::StellarEquilibriumProblem<BaseModel>; using BaseProblem = equilibrium::StellarEquilibriumProblem<BaseModel>;
using CentralDensityProblem = equilibrium::StellarEquilibriumProblem<CentralDensityModel>; using CentralDensityProblem = equilibrium::StellarEquilibriumProblem<CentralDensityModel>;
using AngularMomentumProblem = equilibrium::StellarEquilibriumProblem<AngularMomentumModel>;
using AngularMomentumCentralDensityProblem =
equilibrium::StellarEquilibriumProblem<AngularMomentumCentralDensityModel>;
STATIC_CHECK(equilibrium::StellarEquilibriumModel<BaseModel>); STATIC_CHECK(equilibrium::StellarEquilibriumModel<BaseModel>);
STATIC_CHECK(equilibrium::StellarEquilibriumModel<CentralDensityModel>); STATIC_CHECK(equilibrium::StellarEquilibriumModel<CentralDensityModel>);
STATIC_CHECK(equilibrium::StellarEquilibriumModel<AngularMomentumModel>);
STATIC_CHECK(equilibrium::StellarEquilibriumModel<AngularMomentumCentralDensityModel>);
STATIC_CHECK(equilibrium::StellarEquilibriumModel<EarlierMultiplierModel>);
STATIC_CHECK_FALSE(equilibrium::StellarEquilibriumModel<IncompleteModel>); STATIC_CHECK_FALSE(equilibrium::StellarEquilibriumModel<IncompleteModel>);
STATIC_CHECK_FALSE(operators::StellarEquilibriumRuntimeContribution<
outer_manifest_report_test::EarlierMultiplier>::registered);
STATIC_CHECK_FALSE(operators::stellarEquilibriumBackendRuntimeAuthorized<
outer_manifest_report_test::EarlierMultiplier,
EarlierMultiplierModel>);
STATIC_CHECK(operators::StellarEquilibriumPhysicsAvailableFor<
outer_manifest_report_test::EarlierMultiplier,
EarlierMultiplierModel>);
STATIC_CHECK_FALSE(std::same_as<BaseProblem, CentralDensityProblem>); STATIC_CHECK_FALSE(std::same_as<BaseProblem, CentralDensityProblem>);
STATIC_CHECK(BaseProblem::symbolicallySquare); STATIC_CHECK(BaseProblem::symbolicallySquare);
STATIC_CHECK(CentralDensityProblem::symbolicallySquare); STATIC_CHECK(CentralDensityProblem::symbolicallySquare);
STATIC_CHECK_FALSE(BaseProblem::hasFixedCentralDensity); STATIC_CHECK_FALSE(BaseProblem::hasFixedCentralDensity);
STATIC_CHECK(CentralDensityProblem::hasFixedCentralDensity); STATIC_CHECK(CentralDensityProblem::hasFixedCentralDensity);
STATIC_CHECK(AngularMomentumProblem::hasFixedAngularMomentum);
STATIC_CHECK_FALSE(AngularMomentumProblem::hasFixedCentralDensity);
STATIC_CHECK(AngularMomentumCentralDensityProblem::hasFixedAngularMomentum);
STATIC_CHECK(AngularMomentumCentralDensityProblem::hasFixedCentralDensity);
STATIC_CHECK_FALSE(HasLegacyNumericalModelAdapter<BaseProblem>); STATIC_CHECK_FALSE(HasLegacyNumericalModelAdapter<BaseProblem>);
STATIC_CHECK_FALSE(HasLegacyNumericalModelAdapter<CentralDensityProblem>); STATIC_CHECK_FALSE(HasLegacyNumericalModelAdapter<CentralDensityProblem>);
STATIC_CHECK(std::same_as<BaseProblem, equilibrium::StellarEquilibriumSystem<BaseModel>>); STATIC_CHECK(std::same_as<BaseProblem, equilibrium::StellarEquilibriumSystem<BaseModel>>);
STATIC_CHECK( STATIC_CHECK(
std::same_as<typename BaseProblem::PreparedOperatorType, operators::PreparedStellarEquilibriumOperator> std::same_as<
typename BaseProblem::PreparedOperatorType,
operators::PreparedVariadicStellarEquilibriumOperator<BaseModel>>
); );
STATIC_CHECK( STATIC_CHECK(
std::same_as< std::same_as<
typename CentralDensityProblem::PreparedOperatorType, typename CentralDensityProblem::PreparedOperatorType,
operators::PreparedCentralDensityStellarEquilibriumOperator> operators::PreparedVariadicStellarEquilibriumOperator<CentralDensityModel>>
); );
STATIC_CHECK(
std::same_as<
typename AngularMomentumProblem::PreparedOperatorType,
operators::PreparedVariadicStellarEquilibriumOperator<AngularMomentumModel>>
);
STATIC_CHECK_FALSE(std::same_as<
typename BaseProblem::PreparedOperatorType,
typename CentralDensityProblem::PreparedOperatorType>);
STATIC_CHECK_FALSE(std::same_as<
typename AngularMomentumProblem::PreparedOperatorType,
typename AngularMomentumCentralDensityProblem::PreparedOperatorType>);
STATIC_CHECK(AngularMomentumProblem::FormType::value_block_count == 7);
STATIC_CHECK(AngularMomentumCentralDensityProblem::FormType::value_block_count == 8);
STATIC_CHECK(std::same_as<
typename BaseProblem::FormType,
utils::blocks::surface_deformed_stellar_equilibrium_form>);
STATIC_CHECK(std::same_as<
typename CentralDensityProblem::FormType,
utils::blocks::central_density_bordered_stellar_equilibrium_form>);
STATIC_CHECK( STATIC_CHECK(
std::same_as< std::same_as<
typename BaseProblem::CompiledSurfaceConstraintType, typename BaseProblem::CompiledSurfaceConstraintType,
@@ -97,6 +236,183 @@ TEST_CASE(
STATIC_CHECK(material::CompiledThermodynamicEquations<typename BaseProblem::ThermodynamicEquationsType>); STATIC_CHECK(material::CompiledThermodynamicEquations<typename BaseProblem::ThermodynamicEquationsType>);
} }
TEST_CASE(
"Fixed Angular Momentum Root Uses Its Generated Angular Velocity In Every Physical Row",
"[fixed-angular-momentum][stellar-equilibrium][jacobian][integration]"
) {
using namespace mean_field;
utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
constexpr double radius = utils::RADIUS;
constexpr double mass = utils::MASS;
constexpr double targetAngularMomentum = 0.1;
const double polytropicConstant = 2.0 * utils::G * radius * radius / std::numbers::pi_v<double>;
const double seedCentralDensity =
std::numbers::pi_v<double> * mass / (4.0 * radius * radius * radius);
auto model = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}),
integral::FixedAngularMomentum({
.Jtotal = dimensions::AngularMomentumValue{targetAngularMomentum},
.axis = {0.0, 0.0, 3.0}
})
);
auto problem = equilibrium::discretize(model, finiteElements);
auto projected = seed::makeProjectedEquilibriumState(
problem,
seed::LaneEmden({
.centralDensity = dimensions::DensityValue{seedCentralDensity},
.radialSampleCount = 1024
})
);
auto dependencies = make_dependencies();
const auto preparation = problem.Prepare(projected.values, dependencies);
CHECK(preparation.generatedPhysicalControl);
CHECK(preparation.physical.DidAnyWork());
CHECK(preparation.template specification<models::FixedAngularMomentum>().constraint.DidAnyWork());
CHECK(preparation.template specification<models::FixedAngularMomentum>().generatedRotation);
CHECK(problem.IsPrepared());
const auto angularReport = problem.GetPreparedOperator().GetAngularMomentumReport();
CHECK(angularReport.targetAngularMomentum == targetAngularMomentum);
CHECK(angularReport.angularVelocity > 0.0);
CHECK(angularReport.momentOfInertia > 0.0);
CHECK(std::abs(angularReport.scaledResidual) < 7.0e-4);
mfem::Vector direction(problem.StateSize());
direction = 0.0;
mfem::Vector angularVelocityDirection = problem.GetManifest().stateView(direction).block(
utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term
);
REQUIRE(angularVelocityDirection.Size() == 1);
angularVelocityDirection(0) = -0.37;
angularVelocityDirection.SyncAliasMemory(direction);
mfem::Vector analyticAction;
problem.ApplyLinearization(direction, analyticAction);
constexpr double step = 1.0e-5;
mfem::Vector plusState(projected.values);
plusState.Add(step, direction);
problem.Prepare(plusState, dependencies);
mfem::Vector plusResidual;
problem.BuildResidual(plusResidual);
mfem::Vector minusState(projected.values);
minusState.Add(-step, direction);
problem.Prepare(minusState, dependencies);
mfem::Vector minusResidual;
problem.BuildResidual(minusResidual);
plusResidual -= minusResidual;
plusResidual /= 2.0 * step;
auto analyticView = problem.GetManifest().residualView(analyticAction);
auto differenceView = problem.GetManifest().residualView(plusResidual);
const auto blockError = [&](const auto &term) {
const mfem::Vector analytic = analyticView.block(term);
const mfem::Vector difference = differenceView.block(term);
return relative_difference(analytic, difference);
};
const double surfaceError = blockError(utils::blocks::surface_deformation_field.shape_equilibrium_term);
const double enthalpyError = blockError(utils::blocks::enthalpy_field.specific_term);
const double angularMomentumError =
blockError(utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term);
INFO("Generated-Omega surface-row centered-difference error = " << surfaceError);
INFO("Generated-Omega hydrostatic-row centered-difference error = " << enthalpyError);
INFO("Generated-Omega invariant-row centered-difference error = " << angularMomentumError);
CHECK(surfaceError < 3.0e-7);
CHECK(enthalpyError < 3.0e-7);
CHECK(angularMomentumError < 3.0e-10);
CHECK(analyticView.block(utils::blocks::surface_deformation_field.shape_equilibrium_term).Norml2() > 0.0);
CHECK(analyticView.block(utils::blocks::enthalpy_field.specific_term).Norml2() > 0.0);
CHECK(analyticView.block(utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term).Norml2() > 0.0);
CHECK(analyticView.block(utils::blocks::gravity_field.gradient_term).Norml2() == 0.0);
CHECK(analyticView.block(utils::blocks::gravity_field.poisson_term).Norml2() == 0.0);
CHECK(analyticView.block(utils::blocks::density_field.mass_term).Norml2() == 0.0);
CHECK(analyticView.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term).Norml2() == 0.0);
auto zeroModel = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}),
integral::FixedAngularMomentum({.Jtotal = dimensions::AngularMomentumValue{0.0}})
);
auto zeroProblem = equilibrium::discretize(zeroModel, finiteElements);
mfem::Vector zeroState(projected.values);
zeroProblem.GetManifest().stateView(zeroState).block(
utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term
) = 0.0;
zeroProblem.Prepare(zeroState, dependencies);
mfem::Vector zeroAction;
zeroProblem.ApplyLinearization(direction, zeroAction);
auto zeroView = zeroProblem.GetManifest().residualView(zeroAction);
CHECK(zeroView.block(utils::blocks::surface_deformation_field.shape_equilibrium_term).Norml2() == 0.0);
CHECK(zeroView.block(utils::blocks::enthalpy_field.specific_term).Norml2() == 0.0);
CHECK(zeroView.block(utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term).Norml2() > 0.0);
}
TEST_CASE(
"Fixed Mass Reports Use The Inferred Outer Manifest Indices",
"[stellar-equilibrium][manifest][runtime][ordering]"
) {
using namespace mean_field;
using Form = operators::CompiledStellarEquilibriumForm<EarlierMultiplierModel>;
using EarlierValue = utils::blocks::generated_value_block<
models::MultiplierFor<outer_manifest_report_test::EarlierMultiplier>>;
using MassValue = utils::blocks::fixed_total_mass::mass_normalization::value;
STATIC_CHECK(utils::blocks::type_index_v<
EarlierValue,
typename Form::value_blocks> == 5);
STATIC_CHECK(utils::blocks::type_index_v<
MassValue,
typename Form::value_blocks> == 6);
utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
auto model = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = 0.25}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
outer_manifest_report_test::EarlierMultiplier({
.target = dimensions::SpecificEnergyValue{0.75}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.25}})
);
auto problem = equilibrium::discretize(model, finiteElements);
mfem::Vector state(problem.StateSize());
state = 0.0;
const auto stateView = problem.GetManifest().stateView(state);
stateView.block(utils::blocks::density_field.mass_term) = 1.0;
stateView.block(utils::blocks::enthalpy_field.specific_term) = 1.0;
stateView.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term) = 0.25;
const auto preparation = problem.Prepare(
state,
make_dependencies(),
make_zero_rotation()
);
REQUIRE(preparation.physical.DidAnyWork());
const auto report = problem.GetPreparedOperator().GetFixedMassReport();
const auto &outerDescriptor =
problem.GetManifest().template specification<models::FixedTotalMass>();
CHECK(report.descriptor.stableId == outerDescriptor.stableId);
CHECK(report.descriptor.valueBlock == outerDescriptor.valueBlock);
CHECK(report.descriptor.residualBlock == outerDescriptor.residualBlock);
CHECK(report.descriptor.valueBlock == 6);
CHECK(report.descriptor.residualBlock == 6);
CHECK(report.descriptor.target == 1.25);
CHECK(report.dimensionalResidual == report.achieved - report.descriptor.target);
CHECK(report.scaledResidual ==
report.dimensionalResidual / report.descriptor.residualScale);
}
TEST_CASE( TEST_CASE(
"Discretized Stellar Equilibrium Problem Is Exactly Equivalent To The Legacy Construction Path", "Discretized Stellar Equilibrium Problem Is Exactly Equivalent To The Legacy Construction Path",
tags::stellar_equilibrium_problem_integration tags::stellar_equilibrium_problem_integration
@@ -122,6 +438,7 @@ TEST_CASE(
discretization discretization
); );
auto &modelDrivenOperator = equilibriumProblem.GetPreparedOperator(); auto &modelDrivenOperator = equilibriumProblem.GetPreparedOperator();
const auto &physicalOperator = equilibriumProblem.GetPhysicalOperator();
CHECK(equilibriumProblem.StateSize() == legacyOperator.Width()); CHECK(equilibriumProblem.StateSize() == legacyOperator.Width());
CHECK(equilibriumProblem.EquationSize() == legacyOperator.Height()); CHECK(equilibriumProblem.EquationSize() == legacyOperator.Height());
@@ -129,16 +446,16 @@ TEST_CASE(
CHECK(&equilibriumProblem.GetDiscretization().finiteElementModel() == &f); CHECK(&equilibriumProblem.GetDiscretization().finiteElementModel() == &f);
CHECK(&equilibriumProblem.GetDiscretization().domainMapper() == f.domainMapperStateless.get()); CHECK(&equilibriumProblem.GetDiscretization().domainMapper() == f.domainMapperStateless.get());
CHECK(equilibriumProblem.GetDiscretization().isCurrent()); CHECK(equilibriumProblem.GetDiscretization().isCurrent());
CHECK(modelDrivenOperator.GetTargetMass() == 1.25); CHECK(physicalOperator.GetTargetMass() == 1.25);
CHECK(modelDrivenOperator.GetSurfaceConstraintOperator().GetPhysicalCondition().targetPressure == 0.0); CHECK(physicalOperator.GetSurfaceConstraintOperator().GetPhysicalCondition().targetPressure == 0.0);
CHECK(equilibriumProblem.GetCompiledSurfaceConstraint().targetPressure() == dimensions::PressureValue{0.0}); CHECK(equilibriumProblem.GetCompiledSurfaceConstraint().targetPressure() == dimensions::PressureValue{0.0});
CHECK(modelDrivenOperator.GetDomainDeformation().matchesCurrentDiscretization()); CHECK(physicalOperator.GetDomainDeformation().matchesCurrentDiscretization());
CHECK(&equilibriumProblem.GetLinearizationOperator() == &modelDrivenOperator); CHECK(&equilibriumProblem.GetLinearizationOperator() == &modelDrivenOperator);
CHECK(equilibriumProblem.GetManifest().constraints()[0].target == 1.25); CHECK(equilibriumProblem.GetManifest().template specification<models::FixedTotalMass>().target == 1.25);
mfem::Vector state(legacyOperator.Width()); mfem::Vector state(legacyOperator.Width());
state = 0.0; state = 0.0;
const auto stateView = legacyOperator.GetRootStateView(state); const auto stateView = legacyOperator.GetRootManifest().stateView(state);
stateView.block(utils::blocks::density_field.mass_term) = 1.0; stateView.block(utils::blocks::density_field.mass_term) = 1.0;
stateView.block(utils::blocks::enthalpy_field.specific_term) = 1.0; stateView.block(utils::blocks::enthalpy_field.specific_term) = 1.0;
@@ -163,3 +480,65 @@ TEST_CASE(
equilibriumProblem.ApplyLinearization(direction, modelDrivenAction); equilibriumProblem.ApplyLinearization(direction, modelDrivenAction);
CHECK(relative_difference(modelDrivenAction, legacyAction) < 2.0e-15); CHECK(relative_difference(modelDrivenAction, legacyAction) < 2.0e-15);
} }
TEST_CASE(
"Fixed Angular Momentum Composes With The Optional Central Density Phase At Runtime",
"[fixed-angular-momentum][central-density][stellar-equilibrium][integration]"
) {
using namespace mean_field;
utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
auto model = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = 0.25}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}}),
integral::FixedAngularMomentum({.Jtotal = dimensions::AngularMomentumValue{0.2}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{1.0}})
);
auto problem = equilibrium::discretize(model, finiteElements);
using Problem = std::remove_cvref_t<decltype(problem)>;
STATIC_CHECK(Problem::FormType::value_block_count == 8);
STATIC_CHECK(Problem::FormType::residual_block_count == 8);
mfem::Vector state(problem.StateSize());
state = 0.0;
const auto stateView = problem.GetManifest().stateView(state);
stateView.block(utils::blocks::density_field.mass_term) = 1.0;
stateView.block(utils::blocks::enthalpy_field.specific_term) = 1.0;
stateView.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term) = 0.25;
stateView.block(utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term) = 0.4;
stateView.block(utils::blocks::fixed_central_density_phase.central_value_term) = 0.03;
const auto report = problem.Prepare(state, make_dependencies());
CHECK(report.template specification<models::FixedCentralDensity>().constraint.DidAnyWork());
CHECK(report.template specification<models::FixedAngularMomentum>().constraint.DidAnyWork());
CHECK(problem.IsPrepared());
CHECK(problem.StateSize() == problem.GetPhysicalOperator().Width() + 2);
REQUIRE(problem.GetManifest().constraints().size() == 4);
CHECK(problem.GetManifest().template specification<models::FixedAngularMomentum>().stableId ==
"FixedAngularMomentum");
CHECK(problem.GetManifest().template specification<models::FixedCentralDensity>().stableId ==
"FixedCentralDensity");
mfem::Vector residual;
problem.BuildResidual(residual);
REQUIRE(residual.Size() == problem.EquationSize());
const auto residualView = problem.GetManifest().residualView(residual);
CHECK(std::isfinite(
residualView.block(utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term)(0)
));
CHECK(std::isfinite(residualView.block(utils::blocks::fixed_central_density_phase.central_value_term)(0)));
mfem::Vector direction(problem.StateSize());
for (int index = 0; index < direction.Size(); ++index) {
direction(index) = 0.01 * std::sin(0.17 * static_cast<double>(index + 1));
}
mfem::Vector action;
problem.ApplyLinearization(direction, action);
REQUIRE(action.Size() == problem.EquationSize());
for (int index = 0; index < action.Size(); ++index) {
CHECK(std::isfinite(action(index)));
}
}

View File

@@ -6,6 +6,7 @@
#include <numbers> #include <numbers>
#include <stdexcept> #include <stdexcept>
#include <type_traits> #include <type_traits>
#include <utility>
#include <catch2/catch_test_macros.hpp> #include <catch2/catch_test_macros.hpp>
#include <mfem.hpp> #include <mfem.hpp>
@@ -56,6 +57,33 @@ namespace {
blocks::type_list<>, blocks::type_list<>,
preconditioning::IdentityOperatorCharacteristics, preconditioning::IdentityOperatorCharacteristics,
preconditioning::backend::Identity>; preconditioning::backend::Identity>;
using LifetimeModel = mean_field::model::StellarModel<mean_field::models::SpecificationSet<
mean_field::eos::Polytrope,
mean_field::surface::Isobaric,
mean_field::models::FixedTotalMass,
mean_field::models::FixedCentralDensity>>;
using LifetimeProblem = mean_field::equilibrium::StellarEquilibriumProblem<LifetimeModel>;
using LifetimeBlock = decltype(preconditioning::makePreconditioner(
std::declval<const LifetimeProblem &>()
));
using LifetimePrepared = preconditioning::PreparedStellarPreconditioner<
LifetimeProblem,
LifetimeBlock>;
template <typename Problem, typename Block>
concept CanPrepareStellarPreconditioner = requires(const Problem &problem, Block block) {
preconditioning::prepare(problem, std::move(block));
};
template <typename Problem, typename Block>
concept CanPrepareStellarPreconditionerFromTemporary = requires(Block block) {
preconditioning::prepare(std::declval<Problem &&>(), std::move(block));
};
template <typename Problem, typename Block>
concept CanPrepareStellarPreconditionerFromConstTemporary = requires(Block block) {
preconditioning::prepare(std::declval<const Problem &&>(), std::move(block));
};
[[nodiscard]] blocks::form_layout<Form> makeUnevenLayout() { [[nodiscard]] blocks::form_layout<Form> makeUnevenLayout() {
return { return {
@@ -102,6 +130,12 @@ TEST_CASE(
) { ) {
STATIC_CHECK(preconditioning::EquilibriumCoordinateComponentFor<GroupedComponent, Form>); STATIC_CHECK(preconditioning::EquilibriumCoordinateComponentFor<GroupedComponent, Form>);
STATIC_CHECK_FALSE(preconditioning::EquilibriumCoordinateComponentFor<IncompleteComponent, Form>); STATIC_CHECK_FALSE(preconditioning::EquilibriumCoordinateComponentFor<IncompleteComponent, Form>);
STATIC_CHECK(CanPrepareStellarPreconditioner<LifetimeProblem, LifetimeBlock>);
STATIC_CHECK_FALSE(CanPrepareStellarPreconditionerFromTemporary<LifetimeProblem, LifetimeBlock>);
STATIC_CHECK_FALSE(CanPrepareStellarPreconditionerFromConstTemporary<LifetimeProblem, LifetimeBlock>);
STATIC_CHECK(std::constructible_from<LifetimePrepared, const LifetimeProblem &, LifetimeBlock>);
STATIC_CHECK_FALSE(std::constructible_from<LifetimePrepared, LifetimeProblem &&, LifetimeBlock>);
STATIC_CHECK_FALSE(std::constructible_from<LifetimePrepared, const LifetimeProblem &&, LifetimeBlock>);
const auto layout = makeUnevenLayout(); const auto layout = makeUnevenLayout();
preconditioning::EquilibriumPreconditionerCoordinateMap<Form, GroupedComponent> coordinates(layout); preconditioning::EquilibriumPreconditionerCoordinateMap<Form, GroupedComponent> coordinates(layout);

View File

@@ -2,6 +2,7 @@
#include <array> #include <array>
#include <cmath> #include <cmath>
#include <concepts> #include <concepts>
#include <memory>
#include <numbers> #include <numbers>
#include <stdexcept> #include <stdexcept>
#include <type_traits> #include <type_traits>
@@ -13,6 +14,130 @@
import mean_field; import mean_field;
import test_helpers; import test_helpers;
namespace material_surface_runtime_contract_test {
struct RegisteredAlternateEquationOfState final {
struct Parameters final { };
using ModelDefinition = mean_field::models::ConstitutiveLaw<
RegisteredAlternateEquationOfState,
"RegisteredAlternateMaterialSurfaceEquationOfState">;
using Relations = mean_field::eos::RelationCatalog<mean_field::eos::SpecificEnthalpyFromPressure>;
explicit RegisteredAlternateEquationOfState(Parameters) noexcept { }
[[nodiscard]] mean_field::dimensions::SpecificEnthalpyValue evaluate(
mean_field::eos::SpecificEnthalpyFromPressure,
mean_field::dimensions::PressureValue
) const;
};
class AlternatePhysicalCore final : public mfem::Operator {
public:
using BackendSpecifications = mean_field::models::ModelTypeList<
RegisteredAlternateEquationOfState,
mean_field::surface::Isobaric,
mean_field::models::FixedTotalMass>;
using mfem::Operator::Operator;
void Mult(const mfem::Vector &, mfem::Vector &) const override;
[[nodiscard]] const mean_field::operators::StellarEquilibriumLayout &GetLayout() const noexcept;
[[nodiscard]] mean_field::operators::PreparedStellarEquilibriumReport Prepare(
const mfem::Vector &,
const mean_field::operators::StellarEquilibriumDependencies &,
const mean_field::physics::RigidRotation &
);
void BuildResidual(mfem::Vector &) const;
[[nodiscard]] bool IsPrepared() const noexcept;
[[nodiscard]] mean_field::operators::RootConstraintReport GetFixedMassReport() const;
[[nodiscard]] const mean_field::operators::StellarEquilibriumDependencies &GetDependencies() const;
[[nodiscard]] const mean_field::operators::StellarEquilibriumDependencyStamp &
GetGeneratedDisplacementDependency() const;
[[nodiscard]] const mean_field::operators::PreparedPressureSurfaceConstraint &
GetSurfaceConstraintOperator() const;
};
template <mean_field::model::StellarModelType Model>
class AlternateEquationOfStateRuntime final {
public:
using Report = mean_field::operators::EmptySpecificationPreparationReport;
AlternateEquationOfStateRuntime(
mean_field::fem::FEM &,
const mean_field::mapping::DomainMapper &,
AlternatePhysicalCore &,
const Model &
) noexcept { }
template <typename StateView, typename Controls>
void ReadPhysicalControls(const StateView &, Controls &) noexcept { }
template <typename StateView>
[[nodiscard]] Report PrepareAfterPhysical(
const StateView &,
const mean_field::operators::StellarEquilibriumDependencies &,
const AlternatePhysicalCore &
) noexcept {
return {};
}
template <typename ResidualView>
void AddResidual(const ResidualView &) const noexcept { }
template <typename DirectionView, typename ActionView>
void AddJacobianAction(
const DirectionView &,
const ActionView &,
const AlternatePhysicalCore &
) const noexcept { }
[[nodiscard]] constexpr bool IsPrepared() const noexcept {
return true;
}
};
} // namespace material_surface_runtime_contract_test
namespace mean_field::operators {
template <>
struct StellarEquilibriumCoreRuntime<
material_surface_runtime_contract_test::RegisteredAlternateEquationOfState> {
static constexpr bool registered = true;
using CoreType = material_surface_runtime_contract_test::AlternatePhysicalCore;
[[nodiscard]] static std::unique_ptr<CoreType> Make(
fem::FEM &,
const mapping::DomainMapper &,
const material_surface_runtime_contract_test::RegisteredAlternateEquationOfState &,
const models::CompiledFixedMass &,
PressureSurfaceConstraintView,
deformation::PreparedDomainDeformationRuntime
);
[[nodiscard]] static int SurfaceEquationCount(const CoreType &) noexcept;
};
template <>
struct StellarEquilibriumRuntimeContribution<
material_surface_runtime_contract_test::RegisteredAlternateEquationOfState>
: PreparedStellarEquilibriumContribution<
material_surface_runtime_contract_test::AlternateEquationOfStateRuntime> { };
} // namespace mean_field::operators
namespace mean_field::preconditioning {
/*
* This registration deliberately advertises a distinct core without an
* executable material/surface implementation. Registration alone must
* therefore remain insufficient for MaterialSurfaceRuntimeFor.
*/
template <>
struct MaterialSurfaceEquationOfStateBackend<
material_surface_runtime_contract_test::RegisteredAlternateEquationOfState> {
static constexpr bool registered = true;
using CoreType = material_surface_runtime_contract_test::AlternatePhysicalCore;
};
} // namespace mean_field::preconditioning
namespace { namespace {
namespace backend = mean_field::preconditioning::backend; namespace backend = mean_field::preconditioning::backend;
namespace blocks = mean_field::utils::blocks; namespace blocks = mean_field::utils::blocks;
@@ -25,6 +150,11 @@ namespace {
mean_field::constraint::FixedCentralDensity>>; mean_field::constraint::FixedCentralDensity>>;
using PolytropicProblem = mean_field::equilibrium::StellarEquilibriumProblem<PolytropicModel>; using PolytropicProblem = mean_field::equilibrium::StellarEquilibriumProblem<PolytropicModel>;
using PolytropicMaterialSurfaceDescriptor = preconditioning::MaterialSurfaceDescriptorFor<PolytropicProblem>; using PolytropicMaterialSurfaceDescriptor = preconditioning::MaterialSurfaceDescriptorFor<PolytropicProblem>;
using RegisteredAlternateModel = mean_field::model::StellarModel<mean_field::models::SpecificationSet<
material_surface_runtime_contract_test::RegisteredAlternateEquationOfState,
mean_field::surface::Isobaric,
mean_field::integral::FixedTotalMass>>;
using RegisteredAlternateProblem = mean_field::equilibrium::StellarEquilibriumProblem<RegisteredAlternateModel>;
using MaterialSurfaceDiagonal = preconditioning::MaterialSurfaceBlock< using MaterialSurfaceDiagonal = preconditioning::MaterialSurfaceBlock<
PolytropicMaterialSurfaceDescriptor, PolytropicMaterialSurfaceDescriptor,
backend::Diagonal, backend::Diagonal,
@@ -44,6 +174,30 @@ namespace {
PolytropicMaterialSurfaceDescriptor, PolytropicMaterialSurfaceDescriptor,
preconditioning::ApproximateMaterialSurfaceLDU, preconditioning::ApproximateMaterialSurfaceLDU,
backend::FixedCycles>; backend::FixedCycles>;
using RegisteredAlternateGravityComponent = preconditioning::GravityFieldBlock<
backend::Diagonal,
FixedCycleAMG,
preconditioning::GravityApproximateLDU>;
using RegisteredAlternateMaterialSurfaceDescriptor =
preconditioning::MaterialSurfaceDescriptorFor<RegisteredAlternateProblem>;
struct DistinctPhysicalCore final { };
template <typename Problem>
concept CanMakeDefaultMaterialSurface = requires(const Problem &problem) {
preconditioning::materialSurfaceBlock(problem);
};
template <typename Problem>
concept CanPrepareDefaultMaterialSurface = requires(const Problem &problem) {
preconditioning::prepare(problem, preconditioning::materialSurfaceBlock(problem));
};
template <typename Problem>
concept CanMakeDefaultStellarStructure = requires(const Problem &problem) {
preconditioning::stellarStructureBlock(problem);
};
class KnownCouplings final { class KnownCouplings final {
public: public:
@@ -161,6 +315,53 @@ TEST_CASE(
STATIC_CHECK(preconditioning::PreconditionerComponent<MaterialSurfaceDiagonal>); STATIC_CHECK(preconditioning::PreconditionerComponent<MaterialSurfaceDiagonal>);
STATIC_CHECK(preconditioning::PreconditionerComponent<MaterialSurfaceH1>); STATIC_CHECK(preconditioning::PreconditionerComponent<MaterialSurfaceH1>);
STATIC_CHECK(preconditioning::MaterialSurfaceDescriptor<PolytropicMaterialSurfaceDescriptor>); STATIC_CHECK(preconditioning::MaterialSurfaceDescriptor<PolytropicMaterialSurfaceDescriptor>);
STATIC_CHECK(preconditioning::ImplementedMaterialSurfaceEquationOfState<
mean_field::eos::Polytrope>);
STATIC_CHECK_FALSE(
preconditioning::ImplementedMaterialSurfaceEquationOfState<int>);
STATIC_CHECK(preconditioning::ImplementedMaterialSurfaceDescriptor<
PolytropicMaterialSurfaceDescriptor>);
STATIC_CHECK(preconditioning::ExecutableMaterialSurfaceRuntimeFor<
mean_field::eos::Polytrope,
mean_field::operators::PreparedStellarEquilibriumOperator>);
STATIC_CHECK(std::same_as<
typename preconditioning::MaterialSurfaceEquationOfStateBackend<
mean_field::eos::Polytrope>::CoreType,
mean_field::operators::PreparedStellarEquilibriumOperator>);
STATIC_CHECK(preconditioning::MaterialSurfaceRuntimeFor<
PolytropicMaterialSurfaceDescriptor,
mean_field::operators::PreparedStellarEquilibriumOperator>);
STATIC_CHECK_FALSE(preconditioning::MaterialSurfaceRuntimeFor<
PolytropicMaterialSurfaceDescriptor,
DistinctPhysicalCore>);
STATIC_CHECK(preconditioning::MaterialSurfaceDescriptor<
RegisteredAlternateMaterialSurfaceDescriptor>);
STATIC_CHECK(mean_field::equilibrium::StellarEquilibriumModel<RegisteredAlternateModel>);
STATIC_CHECK(std::same_as<
typename RegisteredAlternateProblem::PhysicalCoreType,
material_surface_runtime_contract_test::AlternatePhysicalCore>);
STATIC_CHECK(preconditioning::ImplementedMaterialSurfaceEquationOfState<
material_surface_runtime_contract_test::RegisteredAlternateEquationOfState>);
STATIC_CHECK(preconditioning::ImplementedMaterialSurfaceDescriptor<
RegisteredAlternateMaterialSurfaceDescriptor>);
STATIC_CHECK_FALSE(preconditioning::ExecutableMaterialSurfaceRuntimeFor<
material_surface_runtime_contract_test::RegisteredAlternateEquationOfState,
material_surface_runtime_contract_test::AlternatePhysicalCore>);
STATIC_CHECK_FALSE(preconditioning::MaterialSurfaceRuntimeFor<
RegisteredAlternateMaterialSurfaceDescriptor,
material_surface_runtime_contract_test::AlternatePhysicalCore>);
STATIC_CHECK(preconditioning::MaterialSurfacePreconditionerProblem<PolytropicProblem>);
STATIC_CHECK_FALSE(preconditioning::MaterialSurfacePreconditionerProblem<RegisteredAlternateProblem>);
STATIC_CHECK(CanMakeDefaultMaterialSurface<PolytropicProblem>);
STATIC_CHECK_FALSE(CanMakeDefaultMaterialSurface<RegisteredAlternateProblem>);
STATIC_CHECK(CanPrepareDefaultMaterialSurface<PolytropicProblem>);
STATIC_CHECK_FALSE(CanPrepareDefaultMaterialSurface<RegisteredAlternateProblem>);
STATIC_CHECK_FALSE(preconditioning::StellarStructurePreconditionerProblem<RegisteredAlternateProblem>);
STATIC_CHECK_FALSE(preconditioning::StellarStructurePreparableFor<
RegisteredAlternateProblem,
MaterialSurfaceDiagonal,
RegisteredAlternateGravityComponent>);
STATIC_CHECK_FALSE(CanMakeDefaultStellarStructure<RegisteredAlternateProblem>);
STATIC_CHECK( STATIC_CHECK(
mean_field::material::CompiledThermodynamicEquations<typename PolytropicProblem::ThermodynamicEquationsType> mean_field::material::CompiledThermodynamicEquations<typename PolytropicProblem::ThermodynamicEquationsType>
); );
@@ -171,7 +372,7 @@ TEST_CASE(
); );
STATIC_CHECK(MaterialSurfaceDiagonal::CorrectionBlocks::size == 3); STATIC_CHECK(MaterialSurfaceDiagonal::CorrectionBlocks::size == 3);
STATIC_CHECK(MaterialSurfaceDiagonal::ResidualBlocks::size == 3); STATIC_CHECK(MaterialSurfaceDiagonal::ResidualBlocks::size == 3);
STATIC_CHECK(MaterialSurfaceDiagonal::RequiredCouplings::size == 8); STATIC_CHECK(MaterialSurfaceDiagonal::RequiredCouplings::size == 9);
STATIC_CHECK(preconditioning::CompletePreconditionerFor<Plan, Form>); STATIC_CHECK(preconditioning::CompletePreconditionerFor<Plan, Form>);
STATIC_CHECK(preconditioning::CompatiblePreconditionerFor<Plan, Form, JacobianForm>); STATIC_CHECK(preconditioning::CompatiblePreconditionerFor<Plan, Form, JacobianForm>);
STATIC_CHECK(preconditioning::backend::ArnoldiAdmissible<typename MaterialSurfaceDiagonal::BackendType>); STATIC_CHECK(preconditioning::backend::ArnoldiAdmissible<typename MaterialSurfaceDiagonal::BackendType>);
@@ -409,7 +610,7 @@ TEST_CASE(
mfem::Vector fullDirection(physical.Width()); mfem::Vector fullDirection(physical.Width());
fullDirection = 0.0; fullDirection = 0.0;
const auto fullDirectionView = physical.GetRootManifest().directionView(fullDirection); const auto fullDirectionView = physical.GetRootManifest().stateView(fullDirection);
const auto &offsets = restricted.GetOffsets(); const auto &offsets = restricted.GetOffsets();
const mfem::Vector densityDirection(restrictedDirection.GetData(), offsets[1]); const mfem::Vector densityDirection(restrictedDirection.GetData(), offsets[1]);
const mfem::Vector surfaceDirection(restrictedDirection.GetData() + offsets[1], offsets[2] - offsets[1]); const mfem::Vector surfaceDirection(restrictedDirection.GetData() + offsets[1], offsets[2] - offsets[1]);

File diff suppressed because it is too large Load Diff

View File

@@ -71,6 +71,10 @@ namespace preconditioning_runtime_test {
++m_snapshot.geometry.revision; ++m_snapshot.geometry.revision;
} }
void AdvancePreparation() noexcept {
++m_snapshot.preparedOperatorGeneration;
}
void SetPrepared(const bool prepared) noexcept { void SetPrepared(const bool prepared) noexcept {
m_prepared = prepared; m_prepared = prepared;
} }
@@ -108,6 +112,136 @@ namespace preconditioning_runtime_test {
}; };
} // namespace preconditioning_runtime_test } // namespace preconditioning_runtime_test
namespace unsupported_physical_preconditioner_test {
class Constraint;
class PreparedConstraint;
class Constraint final {
public:
struct Parameters final {
mean_field::dimensions::SpecificEnthalpyValue target;
};
using TargetValue = mean_field::dimensions::SpecificEnthalpyValue;
using ScalarDescription = mean_field::stellar::ScalarConstraint<
mean_field::dimensions::quantity::SpecificEnthalpy,
mean_field::dimensions::quantity::Dimensionless,
mean_field::dimensions::quantity::SpecificEnthalpy,
"test.unsupported_physical_edge.coordinate",
"q_u",
"test.unsupported_physical_edge.residual",
"R_u">;
using ModelDefinition = mean_field::constraint::ScalarPhaseCondition<
Constraint,
"UnsupportedPhysicalPreconditionerEdge",
mean_field::stellar::Reads<
mean_field::stellar::state::SpecificEnthalpy,
mean_field::stellar::state::OwnGeneratedCoordinate>,
mean_field::stellar::Changes<
mean_field::stellar::equation::PoissonEquation>,
ScalarDescription>;
using EquilibriumPhysics =
mean_field::operators::LocalSpecificationEquilibriumPhysics<
PreparedConstraint>;
explicit constexpr Constraint(const Parameters parameters) noexcept
: m_target(parameters.target) {
}
[[nodiscard]] constexpr TargetValue target() const noexcept {
return m_target;
}
private:
TargetValue m_target;
};
struct PreparationReport final {
bool stateChanged{true};
};
class PreparedConstraint final {
public:
using Report = PreparationReport;
explicit PreparedConstraint(const Constraint &) noexcept {
}
template <typename StateView>
[[nodiscard]] Report PrepareAfterPhysical(const StateView &) noexcept {
m_isPrepared = true;
return {};
}
template <typename Row>
[[nodiscard]] mean_field::stellar::StructuralZero AddResidual(
mean_field::stellar::equation::OwnConstraint,
Row &
) const noexcept {
return mean_field::stellar::structuralZero;
}
template <typename Row>
[[nodiscard]] mean_field::stellar::StructuralZero AddResidual(
mean_field::stellar::equation::PoissonEquation,
Row &
) const noexcept {
return mean_field::stellar::structuralZero;
}
template <typename Direction, typename Row>
[[nodiscard]] mean_field::stellar::StructuralZero AddJacobianAction(
mean_field::stellar::Derivative<
mean_field::stellar::equation::OwnConstraint,
mean_field::stellar::state::SpecificEnthalpy>,
const Direction &,
Row &
) const noexcept {
return mean_field::stellar::zeroDerivative;
}
template <typename Direction, typename Row>
[[nodiscard]] mean_field::stellar::StructuralZero AddJacobianAction(
mean_field::stellar::Derivative<
mean_field::stellar::equation::OwnConstraint,
mean_field::stellar::state::OwnGeneratedCoordinate>,
const Direction &,
Row &
) const noexcept {
return mean_field::stellar::zeroDerivative;
}
template <typename Direction, typename Row>
[[nodiscard]] mean_field::stellar::StructuralZero AddJacobianAction(
mean_field::stellar::Derivative<
mean_field::stellar::equation::PoissonEquation,
mean_field::stellar::state::SpecificEnthalpy>,
const Direction &,
Row &
) const noexcept {
return mean_field::stellar::zeroDerivative;
}
template <typename Direction, typename Row>
[[nodiscard]] mean_field::stellar::StructuralZero AddJacobianAction(
mean_field::stellar::Derivative<
mean_field::stellar::equation::PoissonEquation,
mean_field::stellar::state::OwnGeneratedCoordinate>,
const Direction &,
Row &
) const noexcept {
return mean_field::stellar::zeroDerivative;
}
[[nodiscard]] bool IsPrepared() const noexcept {
return m_isPrepared;
}
private:
bool m_isPrepared{false};
};
} // namespace unsupported_physical_preconditioner_test
template <> struct mean_field::preconditioning::StellarEquilibriumProblemTraits<preconditioning_runtime_test::Problem> { template <> struct mean_field::preconditioning::StellarEquilibriumProblemTraits<preconditioning_runtime_test::Problem> {
using Problem = preconditioning_runtime_test::Problem; using Problem = preconditioning_runtime_test::Problem;
using Form = preconditioning_runtime_test::Form; using Form = preconditioning_runtime_test::Form;
@@ -146,14 +280,40 @@ namespace {
namespace blocks = mean_field::utils::blocks; namespace blocks = mean_field::utils::blocks;
namespace preconditioning = mean_field::preconditioning; namespace preconditioning = mean_field::preconditioning;
using ModelWithoutPhase = mean_field::operators::StellarEquilibriumSpecificationModel; using ModelWithoutPhase = mean_field::model::StellarModel<mean_field::models::SpecificationSet<
using CentralDensityModel = mean_field::operators::CentralDensityStellarEquilibriumSpecificationModel; mean_field::eos::Polytrope,
mean_field::surface::Isobaric,
mean_field::models::FixedTotalMass>>;
using CentralDensityModel = mean_field::model::StellarModel<mean_field::models::SpecificationSet<
mean_field::eos::Polytrope,
mean_field::surface::Isobaric,
mean_field::models::FixedTotalMass,
mean_field::models::FixedCentralDensity>>;
using AngularMomentumModel = mean_field::model::StellarModel<mean_field::models::SpecificationSet<
mean_field::eos::Polytrope,
mean_field::surface::Isobaric,
mean_field::models::FixedTotalMass,
mean_field::models::FixedAngularMomentum>>;
using ProvenZeroPhysicalEdgeModel = mean_field::model::StellarModel<
mean_field::models::SpecificationSet<
mean_field::eos::Polytrope,
mean_field::surface::Isobaric,
mean_field::models::FixedTotalMass,
unsupported_physical_preconditioner_test::Constraint>>;
using ProblemWithoutPhase = mean_field::equilibrium::StellarEquilibriumProblem<ModelWithoutPhase>; using ProblemWithoutPhase = mean_field::equilibrium::StellarEquilibriumProblem<ModelWithoutPhase>;
using CentralDensityProblem = mean_field::equilibrium::StellarEquilibriumProblem<CentralDensityModel>; using CentralDensityProblem = mean_field::equilibrium::StellarEquilibriumProblem<CentralDensityModel>;
using AngularMomentumProblem = mean_field::equilibrium::StellarEquilibriumProblem<AngularMomentumModel>;
using ProvenZeroPhysicalEdgeProblem =
mean_field::equilibrium::StellarEquilibriumProblem<ProvenZeroPhysicalEdgeModel>;
using PlanWithoutPhase = preconditioning::IdentityPreconditionerPlanFor<ProblemWithoutPhase>; using PlanWithoutPhase = preconditioning::IdentityPreconditionerPlanFor<ProblemWithoutPhase>;
using CentralDensityPlan = preconditioning::IdentityPreconditionerPlanFor<CentralDensityProblem>; using CentralDensityPlan = preconditioning::IdentityPreconditionerPlanFor<CentralDensityProblem>;
template <typename Problem>
concept CanMakeDefaultStellarStructureBlock = requires(const Problem &problem) {
preconditioning::stellarStructureBlock(problem);
};
using RefreshingDensityIdentity = preconditioning::ComponentDeclaration< using RefreshingDensityIdentity = preconditioning::ComponentDeclaration<
blocks::type_list<blocks::density::mass::value>, blocks::type_list<blocks::density::mass::value>,
blocks::type_list<blocks::density::mass::residual>, blocks::type_list<blocks::density::mass::residual>,
@@ -203,6 +363,73 @@ TEST_CASE(
STATIC_CHECK(CentralDensityPlan::ComponentTypes::size == 7); STATIC_CHECK(CentralDensityPlan::ComponentTypes::size == 7);
} }
TEST_CASE(
"Default Stellar Structure Availability Distinguishes Proven Zeros From Unhandled Physical Edges",
"[preconditioning][stellar_structure][type_contract][compiler]"
) {
using BaseCompilation =
mean_field::operators::CompiledStellarEquilibriumSystem<ModelWithoutPhase>;
using BaseSupport =
preconditioning::DefaultStellarStructurePhysicalTopologySupport<
ModelWithoutPhase>;
using ProvenZeroSupport =
preconditioning::DefaultStellarStructurePhysicalTopologySupport<
ProvenZeroPhysicalEdgeModel>;
using TrustedFixedMassEdge =
mean_field::operators::StellarEquilibriumJacobianCoupling<
blocks::enthalpy::specific::residual,
blocks::density::mass::value>;
using ProvenZeroPoissonEnthalpyEdge =
mean_field::operators::StellarEquilibriumJacobianCoupling<
blocks::gravity::poisson::residual,
blocks::enthalpy::specific::value>;
// FixedTotalMass contributes h <- rho outside the generic five-field base
// graph. It remains supported because that specification is explicitly
// embedded in the trusted numerical core, not because of a model-pack
// special case.
STATIC_CHECK_FALSE(mean_field::utils::blocks::contains_type_v<
TrustedFixedMassEdge,
typename BaseCompilation::BaseJacobianCouplings>);
STATIC_CHECK(mean_field::utils::blocks::contains_type_v<
TrustedFixedMassEdge,
typename BaseCompilation::ContributionJacobianCouplings>);
STATIC_CHECK(BaseSupport::UnsupportedCouplings::size == 0);
STATIC_CHECK(preconditioning::DefaultStellarStructurePhysicalTopologySupportedFor<
ModelWithoutPhase>);
STATIC_CHECK(preconditioning::DefaultStellarStructurePhysicalTopologySupportedFor<
CentralDensityModel>);
STATIC_CHECK(preconditioning::DefaultStellarStructurePhysicalTopologySupportedFor<
AngularMomentumModel>);
STATIC_CHECK(preconditioning::StellarStructurePreconditionerProblem<
ProblemWithoutPhase>);
STATIC_CHECK(CanMakeDefaultStellarStructureBlock<ProblemWithoutPhase>);
STATIC_CHECK(preconditioning::DefaultStellarPreconditionerAvailableFor<
ProblemWithoutPhase>);
// The mock's novel Poisson <- enthalpy edge is absent from the structure
// backend, but its exact nested provider returns StructuralZero. That is
// a compile-time proof that no preconditioner term is missing; generated-
// coordinate edges are handled independently by the inferred border.
STATIC_CHECK(mean_field::equilibrium::StellarEquilibriumModel<
ProvenZeroPhysicalEdgeModel>);
STATIC_CHECK(mean_field::equilibrium::DiscretizedStellarEquilibriumProblem<
ProvenZeroPhysicalEdgeProblem>);
STATIC_CHECK(ProvenZeroSupport::UnsupportedCouplings::size == 0);
STATIC_CHECK(mean_field::utils::blocks::contains_type_v<
ProvenZeroPoissonEnthalpyEdge,
typename mean_field::operators::CompiledStellarEquilibriumSystem<
ProvenZeroPhysicalEdgeModel>::ContributionJacobianCouplings>);
STATIC_CHECK(preconditioning::DefaultStellarStructurePhysicalTopologySupportedFor<
ProvenZeroPhysicalEdgeModel>);
STATIC_CHECK(preconditioning::StellarStructurePreconditionerProblem<
ProvenZeroPhysicalEdgeProblem>);
STATIC_CHECK(CanMakeDefaultStellarStructureBlock<
ProvenZeroPhysicalEdgeProblem>);
STATIC_CHECK(preconditioning::DefaultStellarPreconditionerAvailableFor<
ProvenZeroPhysicalEdgeProblem>);
}
TEST_CASE( TEST_CASE(
"Prepared Stellar Identity Preconditioning Is Bitwise Equivalent To The P0 Baseline", "Prepared Stellar Identity Preconditioning Is Bitwise Equivalent To The P0 Baseline",
tags::preconditioning_runtime_unit tags::preconditioning_runtime_unit
@@ -286,6 +513,14 @@ TEST_CASE(
CHECK_FALSE(geometryRefresh.changes.linearization); CHECK_FALSE(geometryRefresh.changes.linearization);
CHECK(preconditioner.GetStatistics().refreshes == 2); CHECK(preconditioner.GetStatistics().refreshes == 2);
problem.AdvancePreparation();
CHECK_FALSE(preconditioner.IsCurrent());
const auto preparationRefresh = preconditioner.Refresh();
CHECK(preparationRefresh.changes.linearization);
CHECK_FALSE(preparationRefresh.changes.geometry);
CHECK(preconditioner.IsCurrent());
CHECK(preconditioner.GetStatistics().refreshes == 3);
problem.SetPrepared(false); problem.SetPrepared(false);
CHECK_FALSE(preconditioner.IsCurrent()); CHECK_FALSE(preconditioner.IsCurrent());
CHECK_THROWS_AS(preconditioner.Refresh(), std::logic_error); CHECK_THROWS_AS(preconditioner.Refresh(), std::logic_error);

View File

@@ -114,6 +114,7 @@ namespace {
mean_field::integral::FixedTotalMass, mean_field::integral::FixedTotalMass,
mean_field::constraint::FixedCentralDensity>>; mean_field::constraint::FixedCentralDensity>>;
using PolytropicProblem = mean_field::equilibrium::StellarEquilibriumProblem<PolytropicModel>; using PolytropicProblem = mean_field::equilibrium::StellarEquilibriumProblem<PolytropicModel>;
using PolytropicMaterialSurfaceDescriptor = preconditioning::MaterialSurfaceDescriptorFor<PolytropicProblem>;
using MaterialComponent = using MaterialComponent =
decltype(preconditioning::materialSurfaceBlock(std::declval<const PolytropicProblem &>())); decltype(preconditioning::materialSurfaceBlock(std::declval<const PolytropicProblem &>()));
using FixedAMG = backend::HypreBoomerAMG<backend::FixedCycles>; using FixedAMG = backend::HypreBoomerAMG<backend::FixedCycles>;
@@ -126,6 +127,13 @@ namespace {
preconditioning::IndependentStellarSubsystems{} preconditioning::IndependentStellarSubsystems{}
)); ));
template <typename Problem>
concept CanPrepareDefaultStellarStructure = requires(const Problem &problem) {
preconditioning::prepare(problem, preconditioning::stellarStructureBlock(problem));
};
struct DistinctPhysicalCore final { };
[[nodiscard]] mean_field::operators::StellarEquilibriumDependencies [[nodiscard]] mean_field::operators::StellarEquilibriumDependencies
makeDependencies(const std::uint64_t revision = 1) { makeDependencies(const std::uint64_t revision = 1) {
return { return {
@@ -173,11 +181,26 @@ TEST_CASE(
preconditioning::Coupling<blocks::enthalpy::specific::residual, blocks::gravity::poisson::value>>; preconditioning::Coupling<blocks::enthalpy::specific::residual, blocks::gravity::poisson::value>>;
STATIC_CHECK(preconditioning::PreconditionerComponent<PolytropicStructure>); STATIC_CHECK(preconditioning::PreconditionerComponent<PolytropicStructure>);
STATIC_CHECK(preconditioning::ExecutableStellarStructureRuntimeFor<
mean_field::operators::PreparedStellarEquilibriumOperator>);
STATIC_CHECK_FALSE(preconditioning::ExecutableStellarStructureRuntimeFor<DistinctPhysicalCore>);
STATIC_CHECK(preconditioning::StellarStructureRuntimeFor<
PolytropicMaterialSurfaceDescriptor,
mean_field::operators::PreparedStellarEquilibriumOperator>);
STATIC_CHECK_FALSE(preconditioning::StellarStructureRuntimeFor<
PolytropicMaterialSurfaceDescriptor,
DistinctPhysicalCore>);
STATIC_CHECK(preconditioning::StellarStructurePreconditionerProblem<PolytropicProblem>);
STATIC_CHECK(preconditioning::StellarStructurePreparableFor<
PolytropicProblem,
MaterialComponent,
GravityComponent>);
STATIC_CHECK(CanPrepareDefaultStellarStructure<PolytropicProblem>);
STATIC_CHECK(std::same_as<typename PolytropicStructure::MaterialToGravityCouplings, ExpectedMaterialToGravity>); STATIC_CHECK(std::same_as<typename PolytropicStructure::MaterialToGravityCouplings, ExpectedMaterialToGravity>);
STATIC_CHECK(std::same_as<typename PolytropicStructure::GravityToMaterialCouplings, ExpectedGravityToMaterial>); STATIC_CHECK(std::same_as<typename PolytropicStructure::GravityToMaterialCouplings, ExpectedGravityToMaterial>);
STATIC_CHECK(PolytropicStructure::MaterialToGravityCouplings::size == 3); STATIC_CHECK(PolytropicStructure::MaterialToGravityCouplings::size == 3);
STATIC_CHECK(PolytropicStructure::GravityToMaterialCouplings::size == 2); STATIC_CHECK(PolytropicStructure::GravityToMaterialCouplings::size == 2);
STATIC_CHECK(PolytropicStructure::RequiredCouplings::size == 16); STATIC_CHECK(PolytropicStructure::RequiredCouplings::size == 17);
STATIC_CHECK(preconditioning::backend::ArnoldiAdmissible<typename PolytropicStructure::BackendType>); STATIC_CHECK(preconditioning::backend::ArnoldiAdmissible<typename PolytropicStructure::BackendType>);
using FixedMassIdentity = preconditioning::IdentityBlock< using FixedMassIdentity = preconditioning::IdentityBlock<
@@ -265,7 +288,7 @@ TEST_CASE(
mfem::Vector materialOnlyDirection(physical.Width()); mfem::Vector materialOnlyDirection(physical.Width());
materialOnlyDirection = 0.0; materialOnlyDirection = 0.0;
auto materialOnlyView = physical.GetRootManifest().directionView(materialOnlyDirection); auto materialOnlyView = physical.GetRootManifest().stateView(materialOnlyDirection);
mfem::Vector materialDensity = materialOnlyView.block(blocks::density_field.mass_term); mfem::Vector materialDensity = materialOnlyView.block(blocks::density_field.mass_term);
mfem::Vector materialSurface = materialOnlyView.block(blocks::surface_deformation_field.parameters_term); mfem::Vector materialSurface = materialOnlyView.block(blocks::surface_deformation_field.parameters_term);
mfem::Vector materialEnthalpy = materialOnlyView.block(blocks::enthalpy_field.specific_term); mfem::Vector materialEnthalpy = materialOnlyView.block(blocks::enthalpy_field.specific_term);
@@ -290,7 +313,7 @@ TEST_CASE(
mfem::Vector gravityOnlyDirection(physical.Width()); mfem::Vector gravityOnlyDirection(physical.Width());
gravityOnlyDirection = 0.0; gravityOnlyDirection = 0.0;
auto gravityOnlyView = physical.GetRootManifest().directionView(gravityOnlyDirection); auto gravityOnlyView = physical.GetRootManifest().stateView(gravityOnlyDirection);
mfem::Vector gravityGradient = gravityOnlyView.block(blocks::gravity_field.gradient_term); mfem::Vector gravityGradient = gravityOnlyView.block(blocks::gravity_field.gradient_term);
mfem::Vector gravityPotential = gravityOnlyView.block(blocks::gravity_field.poisson_term); mfem::Vector gravityPotential = gravityOnlyView.block(blocks::gravity_field.poisson_term);
const mfem::Vector sourceGravityGradient( const mfem::Vector sourceGravityGradient(

View File

@@ -13,6 +13,137 @@ import mean_field;
import test_helpers; import test_helpers;
namespace { namespace {
template <typename... Specifications>
using ProjectionModelWith = mean_field::model::StellarModel<
mean_field::models::SpecificationSet<Specifications...>>;
class UnregisteredProjectionConstraint final {
public:
struct Parameters final { };
using ModelDefinition = mean_field::constraint::PhaseCondition<
UnregisteredProjectionConstraint,
"UnregisteredProjectionConstraint",
mean_field::models::DependsOn<mean_field::models::stellar::state::SpecificEnthalpy>,
mean_field::models::Affects<mean_field::models::stellar::equation::HydrostaticBalance>>;
explicit constexpr UnregisteredProjectionConstraint(Parameters) noexcept {
}
};
class ExplicitNoChangeProjectionConstraint final {
public:
struct Parameters final { };
using ModelDefinition = mean_field::constraint::PhaseCondition<
ExplicitNoChangeProjectionConstraint,
"ExplicitNoChangeProjectionConstraint",
mean_field::models::DependsOn<mean_field::models::stellar::state::SpecificEnthalpy>,
mean_field::models::Affects<mean_field::models::stellar::equation::HydrostaticBalance>>;
using RadialProjection = mean_field::seed::projection::Use<mean_field::seed::projection::NoStateChange>;
explicit constexpr ExplicitNoChangeProjectionConstraint(Parameters) noexcept {
}
};
struct IncompleteProjectionPhysics final {
static constexpr bool registered = true;
static constexpr bool providesRadialMass = false;
template <typename Model>
static constexpr bool supports = true;
};
class IncompleteProjectionConstraint final {
public:
struct Parameters final { };
using ModelDefinition = mean_field::constraint::PhaseCondition<
IncompleteProjectionConstraint,
"IncompleteProjectionConstraint",
mean_field::models::DependsOn<mean_field::models::stellar::state::SpecificEnthalpy>,
mean_field::models::Affects<mean_field::models::stellar::equation::HydrostaticBalance>>;
using RadialProjection = mean_field::seed::projection::Use<IncompleteProjectionPhysics>;
explicit constexpr IncompleteProjectionConstraint(Parameters) noexcept {
}
};
class UnregisteredProjectionEquationOfState final {
public:
struct Parameters final { };
using ModelDefinition = mean_field::eos::ConstitutiveLaw<
UnregisteredProjectionEquationOfState,
"UnregisteredProjectionEquationOfState">;
explicit constexpr UnregisteredProjectionEquationOfState(Parameters) noexcept {
}
};
class UnregisteredProjectionSurface final {
public:
struct Parameters final { };
using ModelDefinition = mean_field::surface::BoundaryCondition<
UnregisteredProjectionSurface,
"UnregisteredProjectionSurface">;
explicit constexpr UnregisteredProjectionSurface(Parameters) noexcept {
}
};
struct SecondRadialMassProjectionPhysics final {
static constexpr bool registered = true;
static constexpr bool providesRadialMass = true;
template <typename Model>
static constexpr bool supports = true;
template <typename Specification>
[[nodiscard]] static mean_field::dimensions::MassValue targetMass(const Specification &specification) {
return specification.targetMass();
}
template <typename Specification, typename Model>
static void validate(
const Specification &,
const Model &,
const mean_field::seed::RadialProfile &,
const mean_field::seed::StellarEquilibriumProjectionOptions &
) noexcept {
}
template <typename Specification, typename Model>
static void initialize(
const Specification &,
const Model &,
const mean_field::seed::RadialProjectionScales &,
mean_field::seed::RadialProjectionState &,
mfem::Vector coordinate
) {
if (coordinate.Size() == 1) {
coordinate(0) = 0.0;
}
}
};
class SecondRadialMassConstraint final {
public:
struct Parameters final {
mean_field::dimensions::MassValue mass;
};
using ModelDefinition = mean_field::integral::FixedWithMultiplier<
SecondRadialMassConstraint,
"SecondRadialMassConstraint">;
using RadialProjection = mean_field::seed::projection::Use<SecondRadialMassProjectionPhysics>;
explicit constexpr SecondRadialMassConstraint(Parameters parameters) noexcept : m_mass(parameters.mass) {
}
[[nodiscard]] constexpr mean_field::dimensions::MassValue targetMass() const noexcept {
return m_mass;
}
private:
mean_field::dimensions::MassValue m_mass;
};
[[nodiscard]] mean_field::operators::StellarEquilibriumDependencies make_dependencies() { [[nodiscard]] mean_field::operators::StellarEquilibriumDependencies make_dependencies() {
return { return {
.discretization = {.identity = 7001, .revision = 1}, .discretization = {.identity = 7001, .revision = 1},
@@ -42,6 +173,65 @@ namespace {
} }
} // namespace } // namespace
TEST_CASE(
"Radial Projection Capabilities Are Inferred From Every Model Specification",
tags::stellar_seed_projection_type_contract
) {
using namespace mean_field;
using BaseModel = ProjectionModelWith<eos::Polytrope, surface::Isobaric, integral::FixedTotalMass>;
using CentralModel = ProjectionModelWith<
eos::Polytrope,
surface::Isobaric,
integral::FixedTotalMass,
constraint::FixedCentralDensity>;
using AngularModel = ProjectionModelWith<
eos::Polytrope,
surface::Isobaric,
integral::FixedTotalMass,
integral::FixedAngularMomentum>;
using ExplicitExtensionModel = ProjectionModelWith<
eos::Polytrope,
surface::Isobaric,
integral::FixedTotalMass,
ExplicitNoChangeProjectionConstraint>;
using MissingConstraintRuleModel = ProjectionModelWith<
eos::Polytrope,
surface::Isobaric,
integral::FixedTotalMass,
UnregisteredProjectionConstraint>;
using IncompleteConstraintRuleModel = ProjectionModelWith<
eos::Polytrope,
surface::Isobaric,
integral::FixedTotalMass,
IncompleteProjectionConstraint>;
using MissingEquationOfStateRuleModel = ProjectionModelWith<
UnregisteredProjectionEquationOfState,
surface::Isobaric,
integral::FixedTotalMass>;
using MissingSurfaceRuleModel = ProjectionModelWith<
eos::Polytrope,
UnregisteredProjectionSurface,
integral::FixedTotalMass>;
using MissingMassProviderModel = ProjectionModelWith<eos::Polytrope, surface::Isobaric>;
using AmbiguousMassProviderModel = ProjectionModelWith<
eos::Polytrope,
surface::Isobaric,
integral::FixedTotalMass,
SecondRadialMassConstraint>;
STATIC_CHECK(seed::RadialProfileProjectableModel<BaseModel>);
STATIC_CHECK(seed::RadialProfileProjectableModel<CentralModel>);
STATIC_CHECK(seed::RadialProfileProjectableModel<AngularModel>);
STATIC_CHECK(seed::RadialProfileProjectableModel<ExplicitExtensionModel>);
STATIC_CHECK_FALSE(seed::RadialProfileProjectableModel<MissingConstraintRuleModel>);
STATIC_CHECK_FALSE(seed::RadialProfileProjectableModel<IncompleteConstraintRuleModel>);
STATIC_CHECK_FALSE(seed::RadialProfileProjectableModel<MissingEquationOfStateRuleModel>);
STATIC_CHECK_FALSE(seed::RadialProfileProjectableModel<MissingSurfaceRuleModel>);
STATIC_CHECK_FALSE(seed::RadialProfileProjectableModel<MissingMassProviderModel>);
STATIC_CHECK_FALSE(seed::RadialProfileProjectableModel<AmbiguousMassProviderModel>);
STATIC_CHECK_FALSE(seed::RadialProfileProjectableModel<int>);
}
TEST_CASE( TEST_CASE(
"Projected Equilibrium States Preserve Their Compiled Stellar Model Type", "Projected Equilibrium States Preserve Their Compiled Stellar Model Type",
tags::stellar_seed_projection_type_contract tags::stellar_seed_projection_type_contract
@@ -116,9 +306,9 @@ TEST_CASE(
REQUIRE(centralDensityBorder.Size() == 1); REQUIRE(centralDensityBorder.Size() == 1);
CHECK(centralDensityBorder(0) == 0.0); CHECK(centralDensityBorder(0) == 0.0);
const operators::PreparedCentralDensityStellarEquilibriumReport preparation = const auto preparation = problem.Prepare(projected.values, make_dependencies(), make_zero_rotation());
problem.Prepare(projected.values, make_dependencies(), make_zero_rotation());
CHECK(preparation.assembledResidual); CHECK(preparation.assembledResidual);
CHECK(preparation.template specification<models::FixedCentralDensity>().constraint.DidAnyWork());
mfem::Vector residual; mfem::Vector residual;
problem.BuildResidual(residual); problem.BuildResidual(residual);

View File

@@ -2,6 +2,7 @@
#include <type_traits> #include <type_traits>
#include <catch2/catch_test_macros.hpp> #include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
import mean_field; import mean_field;
import test_helpers; import test_helpers;
@@ -11,6 +12,25 @@ namespace {
const mean_field::utils::Args arguments = test_utils::setup_args(); const mean_field::utils::Args arguments = test_utils::setup_args();
return mean_field::fem::setup_fem(arguments.mesh_file, arguments, 0); return mean_field::fem::setup_fem(arguments.mesh_file, arguments, 0);
} }
template <typename Problem>
concept PreparesWithGeneratedRotation = requires(
Problem &problem,
const mfem::Vector &state,
const mean_field::operators::StellarEquilibriumDependencies &dependencies
) {
problem.Prepare(state, dependencies);
};
template <typename Problem>
concept PreparesWithPrescribedRotation = requires(
Problem &problem,
const mfem::Vector &state,
const mean_field::operators::StellarEquilibriumDependencies &dependencies,
const mean_field::physics::RigidRotation &rotation
) {
problem.Prepare(state, dependencies, rotation);
};
} // namespace } // namespace
TEST_CASE( TEST_CASE(
@@ -42,6 +62,49 @@ TEST_CASE(
CHECK(gravity.potentialSchurBackend().application.cycles == 3); CHECK(gravity.potentialSchurBackend().application.cycles == 3);
} }
TEST_CASE(
"Fixed Angular Momentum User API Generates Rotation And Its Composable Solver Border",
"[user-api][fixed-angular-momentum][type]"
) {
using namespace mean_field;
auto finiteElements = makeFiniteElements();
REQUIRE(finiteElements.okay());
auto model = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = 0.25}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}}),
integral::FixedAngularMomentum({
.Jtotal = dimensions::AngularMomentumValue{0.2},
.axis = {0.0, 0.0, 2.0}
})
);
auto problem = equilibrium::discretize(model, finiteElements);
auto preconditioner = preconditioning::makePreconditioner(problem);
using Problem = std::remove_cvref_t<decltype(problem)>;
using Preconditioner = std::remove_cvref_t<decltype(preconditioner)>;
STATIC_CHECK(Problem::hasFixedAngularMomentum);
STATIC_CHECK_FALSE(Problem::hasFixedCentralDensity);
STATIC_CHECK(PreparesWithGeneratedRotation<Problem>);
STATIC_CHECK_FALSE(PreparesWithPrescribedRotation<Problem>);
STATIC_CHECK(Problem::FormType::value_block_count == 7);
STATIC_CHECK(Problem::FormType::residual_block_count == 7);
STATIC_CHECK(preconditioning::PreconditionerComponent<Preconditioner>);
STATIC_CHECK(Preconditioner::borderValueArity == 2);
STATIC_CHECK(Preconditioner::borderResidualArity == 2);
CHECK(problem.StateSize() == problem.EquationSize());
CHECK(problem.StateSize() == problem.GetPhysicalOperator().Width() + 1);
REQUIRE(problem.GetManifest().constraints().size() == 3);
CHECK(problem.GetManifest().template specification<models::FixedAngularMomentum>().stableId ==
"FixedAngularMomentum");
CHECK(problem.GetManifest().valueBlocks().back().symbol == "Omega");
CHECK(problem.GetManifest().residualBlocks().back().symbol == "R_J");
}
TEST_CASE( TEST_CASE(
"Intermediate User API Selects A Coupled Stellar Factorization", "Intermediate User API Selects A Coupled Stellar Factorization",
"[user-api][intermediate]" "[user-api][intermediate]"