diff --git a/CMakeLists.txt b/CMakeLists.txt index 1c94f17..7aeb884 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -103,7 +103,7 @@ target_sources(mean_field libmeanfield/impl/solver/preconditioning_diagnostics.cpp libmeanfield/impl/preconditioning/gravity_field.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 ) @@ -156,6 +156,11 @@ target_sources(mean_field libmeanfield/interface/preconditioning/specification_border.cppm libmeanfield/interface/preconditioning/equilibrium_coordinates.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_jacobian.cppm libmeanfield/interface/operators/kernels/gravity_kernels.cppm @@ -197,6 +202,7 @@ target_sources(mean_field libmeanfield/interface/models/specifications.cppm libmeanfield/interface/models/typed_stellar_model.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/surface/constant.cppm libmeanfield/interface/surface/dependencies.cppm @@ -214,11 +220,13 @@ target_sources(mean_field libmeanfield/interface/operators/root_manifest.cppm libmeanfield/interface/operators/prepared_constraint.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_centering_constraint.cppm libmeanfield/interface/operators/prepared_surface_constraint.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/operators/stellar_equilibrium_problem.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_displacement_operator.cpp tests/operators/root_manifest.cpp + tests/operators/stellar_equilibrium_compiler.cpp tests/operators/prepared_central_density.cpp tests/operators/prepared_central_density_stellar_equilibrium.cpp tests/models/model_specifications.cpp tests/models/typed_stellar_model.cpp + tests/models/physics_specification_frontend.cpp tests/models/stellar_model.cpp tests/operators/stellar_equilibrium_system.cpp tests/deformation/contracts.cpp @@ -315,6 +325,7 @@ add_executable(tests tests/deformation/radial_extensions.cpp tests/deformation/domain_deformation.cpp tests/operators/prepared_mass_normalization.cpp + tests/operators/prepared_angular_momentum.cpp tests/operators/prepared_stellar_equilibrium.cpp tests/utils/domain.cpp tests/field/field_base.cpp @@ -327,8 +338,12 @@ add_executable(tests tests/preconditioning/material_surface.cpp tests/preconditioning/stellar_structure.cpp tests/preconditioning/specification_border.cpp + tests/extensions/fixed_magnetic_specific_energy.cpp tests/preconditioning/equilibrium_coordinates.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/solver/preconditioning_diagnostics.cpp ) @@ -429,3 +444,8 @@ add_custom_target( DEPENDS mpi_tests 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) diff --git a/experiments/preconditioning_diagnostics.cpp b/experiments/preconditioning_diagnostics.cpp index 33819f4..8a66edc 100644 --- a/experiments/preconditioning_diagnostics.cpp +++ b/experiments/preconditioning_diagnostics.cpp @@ -228,8 +228,7 @@ TEST_CASE( announce(communicator, "P0 extended baseline: preparing the complete equilibrium operator"); const Clock::time_point operatorPreparationStart = Clock::now(); - const operators::PreparedCentralDensityStellarEquilibriumReport preparation = - problem.Prepare(projected.values, make_dependencies(), make_zero_rotation()); + const auto preparation = problem.Prepare(projected.values, make_dependencies(), make_zero_rotation()); REQUIRE(preparation.assembledResidual); const double operatorPreparationSeconds = maximum_rank_seconds(operatorPreparationStart, communicator); diff --git a/extension_example/CMakeLists.txt b/extension_example/CMakeLists.txt new file mode 100644 index 0000000..682f703 --- /dev/null +++ b/extension_example/CMakeLists.txt @@ -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 () diff --git a/extension_example/README.md b/extension_example/README.md new file mode 100644 index 0000000..5b3b9de --- /dev/null +++ b/extension_example/README.md @@ -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. diff --git a/extension_example/demo.cpp b/extension_example/demo.cpp new file mode 100644 index 0000000..e80ed42 --- /dev/null +++ b/extension_example/demo.cpp @@ -0,0 +1,43 @@ +#include +#include + +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( + 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; +} diff --git a/extension_example/ideal_gas_radiation.cppm b/extension_example/ideal_gas_radiation.cppm new file mode 100644 index 0000000..50d9ded --- /dev/null +++ b/extension_example/ideal_gas_radiation.cppm @@ -0,0 +1,403 @@ +module; + +#include +#include +#include + +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; + + /* + * 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 becomes false at + * compile time. + */ + using Relations = mean_field::eos::RelationCatalog< + PressureFromDensityAndTemperature, + SpecificInternalEnergyFromDensityAndTemperature, + SpecificEnthalpyFromDensityAndTemperature + >; + + struct PressureContributions final { + mean_field::dimensions::PressureValue gas; + mean_field::dimensions::PressureValue radiation; + + [[nodiscard]] mean_field::dimensions::PressureValue total() const noexcept { + return gas + radiation; + } + }; + + explicit IdealGasRadiation(const Parameters parameters) + : m_parameters(validatedParameters(parameters)), + m_specificGasConstant( + m_parameters.boltzmannConstant /(m_parameters.meanMolecularWeight * m_parameters.atomicMassUnit) + ) {} + + [[nodiscard]] const Parameters ¶meters() const noexcept { + return m_parameters; + } + + [[nodiscard]] double specificGasConstant() const noexcept { + return m_specificGasConstant; + } + + /* + * Named component functions are not required by the EOS protocol. + * They are provided because they make diagnostics and physics tests + * easier to read than repeated algebra in client code. + */ + [[nodiscard]] PressureContributions pressureContributions( + const mean_field::dimensions::DensityValue density, + const mean_field::dimensions::TemperatureValue temperature + ) const { + validateMaterialState(density, temperature); + + const double rho = density.value(); + const double T = temperature.value(); + return PressureContributions{ + .gas = mean_field::dimensions::PressureValue{rho * m_specificGasConstant * T}, + .radiation = mean_field::dimensions::PressureValue{ + m_parameters.radiationConstant * fourthPower(T) / 3.0 + } + }; + } + + [[nodiscard]] mean_field::dimensions::SpecificInternalEnergyValue gasSpecificInternalEnergy( + const mean_field::dimensions::TemperatureValue temperature + ) const { + validateTemperature(temperature); + return mean_field::dimensions::SpecificInternalEnergyValue{ + 1.5 * m_specificGasConstant * temperature.value() + }; + } + + [[nodiscard]] mean_field::dimensions::SpecificInternalEnergyValue radiationSpecificInternalEnergy( + const mean_field::dimensions::DensityValue density, + const mean_field::dimensions::TemperatureValue temperature + ) const { + validateMaterialState(density, temperature); + return mean_field::dimensions::SpecificInternalEnergyValue{ + m_parameters.radiationConstant * fourthPower(temperature.value()) / density.value() + }; + } + + /* The evaluate overloads implement the three declared relations. */ + [[nodiscard]] mean_field::dimensions::PressureValue evaluate( + PressureFromDensityAndTemperature, + const mean_field::dimensions::DensityValue density, + const mean_field::dimensions::TemperatureValue temperature + ) const { + return pressureContributions(density, temperature).total(); + } + + [[nodiscard]] mean_field::dimensions::SpecificInternalEnergyValue evaluate( + SpecificInternalEnergyFromDensityAndTemperature, + const mean_field::dimensions::DensityValue density, + const mean_field::dimensions::TemperatureValue temperature + ) const { + const auto gas = gasSpecificInternalEnergy(temperature); + const auto radiation = radiationSpecificInternalEnergy(density, temperature); + return gas + radiation; + } + + [[nodiscard]] mean_field::dimensions::SpecificEnthalpyValue evaluate( + SpecificEnthalpyFromDensityAndTemperature, + const mean_field::dimensions::DensityValue density, + const mean_field::dimensions::TemperatureValue temperature + ) const { + validateMaterialState(density, temperature); + + const double rho = density.value(); + const double T = temperature.value(); + return mean_field::dimensions::SpecificEnthalpyValue{ + 2.5 * m_specificGasConstant * T + + 4.0 * m_parameters.radiationConstant * fourthPower(T) / (3.0 * rho) + }; + } + + /* + * Jacobian entries are ordinary analytic partial derivatives. The + * WithRespectTo tag prevents accidentally returning dP/dT from the + * overload that promised dP/drho. + */ + [[nodiscard]] mean_field::eos::PartialDerivative< + eos_quantity::Pressure, + eos_quantity::Density> + partialDerivative( + PressureFromDensityAndTemperature, + mean_field::eos::WithRespectTo, + 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, + 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, + 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, + 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, + 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, + 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); + static_assert(mean_field::eos::EquationOfStateModel); + 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 diff --git a/extension_example/manual/physics_developer_manual.pdf b/extension_example/manual/physics_developer_manual.pdf new file mode 100644 index 0000000..bbc3081 Binary files /dev/null and b/extension_example/manual/physics_developer_manual.pdf differ diff --git a/extension_example/manual/physics_developer_manual.tex b/extension_example/manual/physics_developer_manual.tex new file mode 100644 index 0000000..d68c435 --- /dev/null +++ b/extension_example/manual/physics_developer_manual.tex @@ -0,0 +1,1058 @@ +\documentclass[11pt]{article} + +\usepackage[T1]{fontenc} +\usepackage{lmodern} +\usepackage[margin=0.82in]{geometry} +\usepackage{amsmath} +\usepackage{amssymb} +\usepackage{booktabs} +\usepackage{enumitem} +\usepackage{xcolor} +\usepackage{listings} +\usepackage{tabularx} +\usepackage{longtable} +\usepackage{fancyhdr} +\usepackage{microtype} +\usepackage{hyperref} + +\definecolor{MeanFieldBlue}{HTML}{174A67} +\definecolor{MeanFieldLightBlue}{HTML}{EAF3F8} +\definecolor{MeanFieldGold}{HTML}{A66500} +\definecolor{CodeBackground}{HTML}{F5F7F8} +\definecolor{CodeComment}{HTML}{476B45} +\definecolor{CodeKeyword}{HTML}{6B327A} + +\hypersetup{ + colorlinks=true, + linkcolor=MeanFieldBlue, + urlcolor=MeanFieldBlue, + pdftitle={MeanField Physics Developer Manual}, + pdfauthor={MeanField extension example} +} + +\pagestyle{fancy} +\fancyhf{} +\lhead{MeanField Physics Developer Manual} +\rhead{Extension Example} +\cfoot{\thepage} +\setlength{\headheight}{14pt} + +\setlist[itemize]{leftmargin=1.35em,itemsep=0.25em,topsep=0.35em} +\setlist[enumerate]{leftmargin=1.5em,itemsep=0.35em,topsep=0.35em} + +\lstdefinestyle{meanfieldcpp}{ + language=C++, + basicstyle=\ttfamily\small, + keywordstyle=\color{CodeKeyword}\bfseries, + commentstyle=\color{CodeComment}, + stringstyle=\color{MeanFieldGold}, + backgroundcolor=\color{CodeBackground}, + frame=single, + rulecolor=\color{MeanFieldBlue!35}, + numbers=left, + numberstyle=\tiny\color{black!55}, + numbersep=7pt, + breaklines=true, + breakatwhitespace=true, + columns=fullflexible, + keepspaces=true, + showstringspaces=false, + tabsize=4 +} + +\lstdefinestyle{meanfieldshell}{ + basicstyle=\ttfamily\small, + backgroundcolor=\color{CodeBackground}, + frame=single, + rulecolor=\color{MeanFieldBlue!35}, + breaklines=true, + columns=fullflexible, + keepspaces=true, + showstringspaces=false +} + +\newcommand{\MeanField}{\textnormal{\textsc{MeanField}}} +\newcommand{\code}[1]{\texttt{#1}} +\newcommand{\dd}{\mathrm{d}} +\newcommand{\pd}[2]{\frac{\partial #1}{\partial #2}} + +\newenvironment{developerbox} +{\begin{center}\begin{minipage}{0.94\textwidth}\color{MeanFieldBlue}\hrule\vspace{0.55em}\color{black}} +{\vspace{0.55em}\color{MeanFieldBlue}\hrule\end{minipage}\end{center}} + +\title{\vspace{-1.5em}\color{MeanFieldBlue}\Huge\bfseries + Developing Physics Extensions for \MeanField\\[0.4em] + \Large A worked ideal-gas plus radiation equation of state} +\author{Physics developer example module} +\date{Code checkpoint \code{71423d543f1775d3b943d9c0361f49edd56b59c1}\\ + Manual built 2026-09-06} + +\begin{document} + +\maketitle + +\begin{developerbox} +\textbf{What this manual promises.} +You will implement a real two-variable equation of state, make its physical +relations and Jacobian derivatives compile-time contracts, and compose it with +the existing isobaric surface, fixed-total-mass invariant, and +fixed-angular-momentum invariant. You do not need to write block indices or +repeat the model assembly logic. + +\medskip +\textbf{What this manual does not claim.} +The current numerical stellar-equilibrium core is barotropic. The worked EOS +depends independently on density and temperature. It therefore forms a valid +typed stellar-model specification, but it cannot yet be passed to the current +\code{discretize} operation. A temperature or entropy field and a governing +thermal equation must be added to the equilibrium formulation first. The +example encodes this boundary as a compile-time assertion. +\end{developerbox} + +\tableofcontents +\clearpage + +\section{Audience and objective} + +This manual is for an astronomer or physicist who knows the equations they want +to add but may not be interested in the template machinery used to assemble a +nonlinear root system. The central design rule is: + +\begin{quote} +State the physics once, in physics vocabulary. Let the library infer the +structural type of the model, residual, Jacobian, normalization plan, and +preconditioner wherever a runtime implementation exists. If a required piece +does not exist, reject the operation at compile time. +\end{quote} + +The worked extension lives entirely under \code{extension\_example/}. It does +not modify a source file in \code{libmeanfield/}. This separation matters. It +demonstrates the public extension surface rather than relying on privileged +access to the implementation. + +At the end, you should be able to answer four questions for a proposed physics +extension: + +\begin{enumerate} + \item What quantities and relations does the new physics provide? + \item Which analytic derivatives does a Newton Jacobian require? + \item Which existing specifications can be composed with it immediately? + \item Is the complete numerical runtime available, or is only the symbolic + model declaration available? +\end{enumerate} + +\section{The mental model: five layers} + +It helps to separate five ideas that are often blended together in scientific +software. + +\begin{longtable}{p{0.16\textwidth} p{0.31\textwidth} p{0.43\textwidth}} +\toprule +\textbf{Layer} & \textbf{Physics question} & \textbf{MeanField representation} \\ +\midrule +\endhead +Quantity & What does this scalar mean? & A type such as +\code{Density}, \code{Temperature}, or \code{Pressure}, wrapped in a +\code{QuantityValue}. \\ +\addlinespace +Relation & Which variables determine which result? & A compile-time sentence +such as $P=P(\rho,T)$, represented by \code{eos::Relation}. \\ +\addlinespace +Specification & What physical choice did the model make? & A concrete EOS, +surface condition, invariant, phase condition, gauge, or rotation law with a +small \code{ModelDefinition}. \\ +\addlinespace +Compiled system & Which unknowns, residuals, and Jacobian couplings follow from +the selected specifications? & A type inferred from the complete, variadic +specification pack. \\ +\addlinespace +Runtime provider & How are those residuals and derivatives evaluated on a +mesh? & Numerical assembly, normalization, and preconditioning code that must +exist for the compiled type. \\ +\bottomrule +\end{longtable} + +A model may be valid at one layer and intentionally unavailable at a later +layer. In this example, $P(\rho,T)$ is a valid EOS relation and the four chosen +specifications form a valid stellar-model type. The present runtime provider +has a barotropic material core, so it rejects the thermal model before a +discretization object can be formed. + +This distinction prevents two dangerous outcomes: + +\begin{itemize} + \item silently dropping a supplied physical constraint; and + \item pretending a multidimensional EOS is barotropic by choosing an + undocumented path through thermodynamic state space. +\end{itemize} + +\section{Map of the example module} + +\begin{tabularx}{\textwidth}{p{0.37\textwidth} X} +\toprule +\textbf{File} & \textbf{Purpose} \\ +\midrule +\code{ideal\_gas\_radiation.cppm} & Declares relations, implements the EOS, +analytic derivatives, input validation, and compile-time protocol checks. \\ +\code{rotating\_stellar\_model.cppm} & Provides the small physics-facing +composition wrapper and documents the current runtime boundary. \\ +\code{demo.cpp} & Evaluates one CGS thermodynamic state and constructs the +typed stellar-model specification. \\ +\code{tests/ideal\_gas\_radiation.cpp} & Tests dimensions, thermodynamic +identities, scaling laws, derivative accuracy, and domain handling. \\ +\code{tests/rotating\_stellar\_}\newline\code{model.cpp} & Tests inferred roles, exact +specification types, values, and capability rejection. \\ +\code{manual/physics\_developer\_}\newline\code{manual.tex} & The source of this manual. \\ +\bottomrule +\end{tabularx} + +The module has its own library, demonstration executable, and test executable. +It links to \MeanField, but its files are not added to the \code{mean\_field} +library target and its tests are not added to the main regression executable. + +\section{Worked physics: ideal gas plus radiation} + +\subsection{Assumptions} + +The example describes a deliberately simple local thermodynamic model: + +\begin{itemize} + \item matter is a classical, nondegenerate, monatomic ideal gas; + \item the mean molecular weight $\mu$ is fixed; + \item gas and radiation share a single temperature $T$; + \item radiation is in local thermodynamic equilibrium; + \item ionization, composition evolution, pairs, degeneracy, Coulomb + corrections, and opacity do not enter the EOS. +\end{itemize} + +These assumptions are appropriate for demonstrating the extension protocol. +They are not a universal stellar EOS. + +Let $k_{\mathrm B}$ be Boltzmann's constant, $m_{\mathrm u}$ the atomic mass +unit, $a$ the radiation energy-density constant, and + +\begin{equation} + \mathcal{R} = \frac{k_{\mathrm B}}{\mu m_{\mathrm u}} +\end{equation} + +the gas constant per unit mass. The pressure contributions are + +\begin{align} + P_{\mathrm{gas}} &= \rho \mathcal{R} T, \\ + P_{\mathrm{rad}} &= \frac{aT^4}{3}, \\ + P(\rho,T) &= P_{\mathrm{gas}} + P_{\mathrm{rad}}. +\end{align} + +For a monatomic gas, the specific internal energies are + +\begin{align} + u_{\mathrm{gas}} &= \frac{3}{2}\mathcal{R}T, \\ + u_{\mathrm{rad}} &= \frac{aT^4}{\rho}, \\ + u(\rho,T) &= u_{\mathrm{gas}} + u_{\mathrm{rad}}. +\end{align} + +Using $h=u+P/\rho$, the specific enthalpy is + +\begin{equation} + h(\rho,T) + = \frac{5}{2}\mathcal{R}T + + \frac{4aT^4}{3\rho}. +\end{equation} + +\subsection{Analytic derivatives} + +A Newton method needs derivatives of the residual with respect to its state. +For this EOS, the useful local derivatives are + +\begin{align} +\left.\pd{P}{\rho}\right|_T + &= \mathcal{R}T, & +\left.\pd{P}{T}\right|_\rho + &= \rho\mathcal{R}+\frac{4aT^3}{3}, \\ +\left.\pd{u}{\rho}\right|_T + &= -\frac{aT^4}{\rho^2}, & +\left.\pd{u}{T}\right|_\rho + &= \frac{3}{2}\mathcal{R}+\frac{4aT^3}{\rho}, \\ +\left.\pd{h}{\rho}\right|_T + &= -\frac{4aT^4}{3\rho^2}, & +\left.\pd{h}{T}\right|_\rho + &= \frac{5}{2}\mathcal{R}+\frac{16aT^3}{3\rho}. +\end{align} + +The subscript is not optional notation. It tells the developer what is held +fixed and corresponds directly to the \code{WithRespectTo<...>} tag in the +code. A derivative with the right numerical return type but the wrong held +variable is a physics bug. + +\subsection{Domain and units} + +The implemented material domain is + +\begin{equation} + \rho > 0, \qquad T \geq 0. +\end{equation} + +Positive density is required because the specific radiation energy and +enthalpy contain $1/\rho$. The radiation constant may be set to zero for a +pure-gas verification problem. Other constants and $\mu$ must be finite and +positive. + +\MeanField's \code{QuantityValue} gives semantic type safety. It does not +perform dimensional algebra or unit conversion. The defaults in this example +are CGS: + +\begin{center} +\begin{tabular}{lll} +\toprule +Constant & Default & CGS unit \\ +\midrule +$k_{\mathrm B}$ & $1.380649\times10^{-16}$ & erg K$^{-1}$ \\ +$m_{\mathrm u}$ & $1.66053906660\times10^{-24}$ & g \\ +$a$ & $7.5657\times10^{-15}$ & erg cm$^{-3}$ K$^{-4}$ \\ +\bottomrule +\end{tabular} +\end{center} + +If the rest of a model uses nondimensional or code units, all three constants, +density, temperature, and returned thermodynamic values must be transformed +coherently. A typed value prevents passing temperature where density belongs; +it cannot detect a CGS value mixed with a nondimensional one. + +\section{Implementing the EOS} + +\subsection{Step 1: name the relations} + +The first code says what the EOS knows, without saying how it computes it. + +\begin{lstlisting}[style=meanfieldcpp] +namespace q = mean_field::dimensions::quantity; + +using PressureFromDensityAndTemperature = mean_field::eos::Relation< + q::Pressure, + q::Density, + q::Temperature>; + +using SpecificEnthalpyFromDensityAndTemperature = mean_field::eos::Relation< + q::SpecificEnthalpy, + q::Density, + q::Temperature>; +\end{lstlisting} + +The first template argument is the output. Remaining arguments are inputs in +call order. Consequently, + +\begin{lstlisting}[style=meanfieldcpp] +eos::evaluate(eosModel, density, temperature); +\end{lstlisting} + +is valid, while the same call with \code{temperature, density} is rejected at +compile time. + +Use an existing quantity type when it has the intended physical meaning. +Temperature and specific internal energy already exist in +\code{mean\_field::dimensions::quantity}. If a genuinely new quantity is +needed, it can derive from \code{ThermodynamicQuantity} and provide a stable +identifier, but adding a quantity to numerical normalization also requires an +appropriate scale law and runtime support. + +\subsection{Step 2: identify the specification role} + +Inside the EOS class, one alias connects the physics type to the stellar-model +front end: + +\begin{lstlisting}[style=meanfieldcpp] +using ModelDefinition = mean_field::eos::ConstitutiveLaw< + IdealGasRadiation, + "IdealGasRadiation">; +\end{lstlisting} + +The stable name is used in compile-time keys and runtime descriptions. Choose +a specific, durable name. Do not encode parameter values in it, and do not +reuse the same role-name pair for unrelated physics. + +This alias is the wrapper intended for physics developers. It projects to the +more general model-definition machinery, but the EOS author does not need to +name generated blocks, residual rows, or a normalization topology because a +constitutive law owns no scalar solver border by itself. + +\subsection{Step 3: publish the complete relation catalog} + +\begin{lstlisting}[style=meanfieldcpp] +using Relations = mean_field::eos::RelationCatalog< + PressureFromDensityAndTemperature, + SpecificInternalEnergyFromDensityAndTemperature, + SpecificEnthalpyFromDensityAndTemperature>; +\end{lstlisting} + +Treat this catalog as an API promise. Every listed relation must have an exact +\code{evaluate} overload. A relation that exists as a helper function but is +missing from the catalog is not visible to generic consumers. A relation in +the catalog without a matching evaluator makes \code{EquationOfStateModel} +false. + +\subsection{Step 4: implement evaluations in physics notation} + +The pressure overload is small because named component functions expose useful +diagnostics: + +\begin{lstlisting}[style=meanfieldcpp] +[[nodiscard]] dimensions::PressureValue evaluate( + PressureFromDensityAndTemperature, + dimensions::DensityValue density, + dimensions::TemperatureValue temperature +) const { + return pressureContributions(density, temperature).total(); +} +\end{lstlisting} + +The relation tag is an empty compile-time object. It selects the overload but +does not cost storage. Return the exact typed value requested by the relation. +Returning a raw \code{double}, even with the correct number, fails the EOS +concept. + +The generic user-facing call is + +\begin{lstlisting}[style=meanfieldcpp] +const auto pressure = mean_field::eos::evaluate< + mean_field::dimensions::quantity::Pressure>( + equationOfState, + density, + temperature + ); +\end{lstlisting} + +This call infers the relation from output type and typed input sequence. Client +code does not construct relation tags directly. + +\subsection{Step 5: implement Jacobian derivatives} + +Each derivative overload repeats the relation and states the differentiation +variable explicitly: + +\begin{lstlisting}[style=meanfieldcpp] +[[nodiscard]] eos::PartialDerivative +partialDerivative( + PressureFromDensityAndTemperature, + eos::WithRespectTo, + dimensions::DensityValue density, + dimensions::TemperatureValue temperature +) const { + const double T = temperature.value(); + return eos::PartialDerivative{ + density.value() * specificGasConstant() + + 4.0 * parameters().radiationConstant * T * T * T / 3.0 + }; +} +\end{lstlisting} + +The important contracts are: + +\begin{itemize} + \item relation inputs occur in the same order as in \code{evaluate}; + \item \code{WithRespectTo} identifies the independent + variable; + \item the return type is exactly + \code{PartialDerivative}; and + \item the formula is a partial derivative at fixed density. +\end{itemize} + +\subsection{Step 6: make the compiler check the public claim} + +The source finishes with assertions such as + +\begin{lstlisting}[style=meanfieldcpp] +static_assert(eos::EquationOfStateModel); + +static_assert(eos::SupportsPartialDerivative< + IdealGasRadiation, + PressureFromDensityAndTemperature, + q::Temperature>); +\end{lstlisting} + +These assertions are not substitutes for numerical tests. They prove shape: +the relation is declared, the argument order matches, and the exact output type +exists. Numerical tests must still prove that the derivative computes the +right mathematics. + +\section{Composing a rotating stellar model} + +The model itself is the direct expression of the physical choices: + +\begin{lstlisting}[style=meanfieldcpp] +auto model = mean_field::model::StellarModel( + IdealGasRadiation(eosParameters), + mean_field::surface::Isobaric({.Psurf = surfacePressure}), + mean_field::integral::FixedTotalMass({.Mtotal = totalMass}), + mean_field::integral::FixedAngularMomentum({ + .Jtotal = totalAngularMomentum, + .axis = rotationAxis, + .center = rotationCenter + }) +); +\end{lstlisting} + +The example wraps this expression in \code{makeRotatingStellarModel}. The +wrapper groups frequently used inputs; it does not duplicate model assembly. +An advanced caller can always use \code{StellarModel(...)} directly. + +The compiler infers four distinct specification types and their roles: + +\begin{center} +\begin{tabularx}{0.94\textwidth}{p{0.28\textwidth} p{0.21\textwidth} X} +\toprule +Specification & Role & Structural contribution \\ +\midrule +\code{IdealGasRadiation} & constitutive law & EOS relations; no generated +scalar coordinate \\ +\code{Isobaric} & boundary condition & fixed surface pressure; no generated +scalar coordinate \\ +\code{FixedTotalMass} & invariant & mass constraint residual and its scalar +multiplier \\ +\code{FixedAngularMomentum} & invariant & angular-momentum constraint residual +and angular velocity as a physical coordinate \\ +\bottomrule +\end{tabularx} +\end{center} + +The two invariants each contribute one scalar unknown and one scalar residual. +The complete symbolic operator remains square. Reordering the constructor +arguments does not define a new physical combination: the library canonicalizes +the specification set by stable compile-time keys while storing exactly one +object of each inferred type. + +\begin{samepage} +The model exposes physics-facing accessors: + +\begin{lstlisting}[style=meanfieldcpp] +model.equationOfState(); +model.surfaceCondition(); +model.specification(); +model.specification(); +\end{lstlisting} + +These preserve exact types. There is no base-class downcast and no string +lookup in the inner numerical path. +\end{samepage} + +\section{What compiles today, and why} + +\subsection{The three useful questions} + +For a new model, ask these separately: + +\begin{enumerate} + \item \textbf{Is each object a valid specification?} The type has a valid + role definition, parameters, and contribution metadata. + \item \textbf{Can the objects form a stellar-model type?} Their stable keys + are unique, exactly one constitutive law is present, and generated + scalar arities make a square symbolic system. + \item \textbf{Can the current numerical backend discretize that exact + type?} Every core equation, surface conversion, residual assembly, + Jacobian coupling, normalization action, and runtime provider exists. +\end{enumerate} + +For this example, the answers are yes, yes, and no. + +\subsection{Why the current core rejects this EOS} + +The current stellar material core is barotropic. Its state can close density +through a relation of the form + +\begin{equation} + \rho = \rho(h), +\end{equation} + +with the corresponding derivative $\dd\rho/\dd h$. This is sufficient for a +polytropic barotrope. + +The example instead supplies + +\begin{equation} + P=P(\rho,T), \qquad u=u(\rho,T), \qquad h=h(\rho,T). +\end{equation} + +Hydrostatic balance plus an isobaric surface does not determine both $\rho$ +and $T$ throughout the star. A second physical equation is required. Depending +on the intended model, it might be an entropy prescription, radiative energy +transport, convective closure, or a full energy equation with sources and +sinks. + +The example therefore includes + +\begin{lstlisting}[style=meanfieldcpp] +static_assert(model::StellarModelType); +static_assert(!equilibrium::StellarEquilibriumModel); +\end{lstlisting} + +The second assertion is a capability tripwire. When a thermal equilibrium core +is eventually implemented, the assertion and this manual must be revised +together. + +\subsection{What must be added for a coupled thermal solve} + +A physically complete extension will need all of the following, designed as a +single compile-time path rather than a special case for this EOS: + +\begin{enumerate} + \item A selected thermal state coordinate, such as temperature or specific + entropy, with a finite-element field family. + \item A thermal residual equation whose physical closure is explicit. + \item EOS relations and partial derivatives required by both hydrostatic + and thermal residuals. + \item Surface data appropriate to the thermal equation, such as a fixed + temperature, luminosity, or atmosphere matching condition. + \item Generated block and Jacobian couplings inferred from the selected + thermodynamic equation set. + \item Normalization metadata for the new field and residual, including a + physically meaningful Riesz map and scale. + \item A preconditioner component that approximates the new thermal block + and its important coupling to structure. + \item Runtime providers for assembly and linearization of every inferred + contribution. +\end{enumerate} + +The EOS in this module is already expressed in the multi-input relation +vocabulary needed by such a future core. It should not be rewritten as a +bespoke EOS-backend combination. + +\section{Testing a physics extension} + +A robust extension test suite has four layers. Passing only the first layer is +not enough. + +\subsection{Compile-time protocol tests} + +These answer structural questions: + +\begin{itemize} + \item Is the class a self-describing constitutive-law specification? + \item Does every catalog relation have an exact evaluator? + \item Are required partial derivatives available? + \item Are input order and output quantity types enforced? + \item Does composition preserve the exact EOS, surface, and invariant + types? + \item Is the inferred scalar root system square? +\end{itemize} + +The reversed pressure call $(T,\rho)$ is tested as a non-callable expression. +This is a useful negative contract: it proves that strong quantity types catch +an error that two raw \code{double} arguments would not catch. + +\subsection{Exact worked state} + +The tests choose artificial constants that make hand calculation easy: + +\begin{equation} + \mu=2, \quad k_{\mathrm B}=12, \quad m_{\mathrm u}=3, + \quad a=9, +\end{equation} + +so $\mathcal{R}=2$. At $\rho=4$ and $T=2$, + +\begin{align} + P_{\mathrm{gas}} &= 16, & P_{\mathrm{rad}} &= 48, & P &= 64,\\ + u_{\mathrm{gas}} &= 6, & u_{\mathrm{rad}} &= 36, & u &= 42,\\ + h &= u + P/\rho = 42 + 16 = 58. +\end{align} + +This single state catches wrong factors of $1/3$, $3/2$, $4/3$, and $5/2$. +It also tests the independent thermodynamic identity $h=u+P/\rho$. + +\subsection{Scaling and limiting behavior} + +The tests verify transformations more diagnostic than a list of isolated +numbers: + +\begin{itemize} + \item doubling $\rho$ doubles $P_{\mathrm{gas}}$; + \item changing $\rho$ leaves $P_{\mathrm{rad}}$ unchanged; + \item doubling $T$ doubles $P_{\mathrm{gas}}$; + \item doubling $T$ multiplies $P_{\mathrm{rad}}$ by 16; and + \item at + $T_{\mathrm{cross}}=(3\rho\mathcal{R}/a)^{1/3}$, gas and radiation + pressures agree. +\end{itemize} + +These claims exercise the physical exponents and parameter dependencies. They +would fail even if a few reference values happened to be fitted correctly. + +\subsection{Analytic versus numerical derivatives} + +Every one of the six analytic partial derivatives is compared with a centered +difference at three materially different states. For a scalar function $f$, + +\begin{equation} + f'(x) \approx \frac{f(x+\epsilon)-f(x-\epsilon)}{2\epsilon}. +\end{equation} + +The test uses + +\begin{equation} + \epsilon = \epsilon_{\mathrm{machine}}^{1/3} + \max(1,|x|). +\end{equation} + +For a centered difference, this scale balances truncation and roundoff for +smooth double-precision functions. The comparison tolerance is selected from +the observed error order and is much tighter than a solver tolerance. It tests +the local Jacobian implementation, not convergence of a nonlinear solve. + +When extending a tabulated or iterative EOS, use tolerances justified by that +algorithm's interpolation and inner-solve error. Do not copy the analytic-EOS +tolerance mechanically. + +\subsection{Domain tests} + +Tests require rejection of invalid constants, nonfinite values, zero density, +and negative temperature. Domain tests should check error categories where a +caller can recover differently from a nonfinite input and an out-of-domain +state. + +\section{Adding other kinds of physics} + +The same pattern extends beyond an EOS: declare a physics-facing wrapper, state +dependencies and effects once, and let the compiler generate structure. The +details below are a map, not a replacement for tests of the actual equations. + +\subsection{Surface conditions} + +A surface condition uses + +\begin{lstlisting}[style=meanfieldcpp] +using ModelDefinition = mean_field::surface::BoundaryCondition< + MySurfaceCondition, + "MySurfaceCondition">; +\end{lstlisting} + +It should name its physical target quantity and typed target value. A new +surface condition can enter a \code{StellarModel} as a distinct type before a +runtime surface compiler exists. Full discretization is available only if the +chosen EOS can convert the boundary statement into the thermodynamic variable +used by the selected material formulation. + +For example, an isothermal surface is physically meaningful for a thermal +model. It does not become meaningful for a barotropic formulation merely +because both types compile as specifications. + +\subsection{Integral invariants} + +An invariant such as fixed mass or fixed angular momentum owns a global scalar +equation and usually a conjugate scalar coordinate. Physics-facing aliases +include \code{FixedScalarWithMultiplier} and +\code{FixedScalarWithPhysicalCoordinate}. Their declarations identify: + +\begin{itemize} + \item the target physical quantity; + \item the generated coordinate quantity; + \item the constraint residual quantity; + \item which state quantities the integral reads; + \item which equations the generated coordinate changes; + \item stable symbols and identifiers; and + \item normalization metadata. +\end{itemize} + +That information is enough to generate the border shape and required Jacobian +incidence. A runtime provider must still implement the integral, its derivative, +and its action on affected equations. If that provider is absent, compilation +must stop at the operation boundary rather than ignore the invariant. + +\subsection{Phase conditions} + +A phase condition removes a neutral direction or selects one representative of +a family. It is not necessarily a conserved physical quantity. Fixed central +density, for example, can serve as a phase constraint while fixed total mass is +the physically informed invariant. + +Use \code{ScalarPhaseCondition} for the physics-facing declaration. Keep the +distinction explicit in naming, documentation, and tests: an invariant changes +the physical problem, while a phase condition selects a solution within a +degenerate representation. + +\section{Normalization and preconditioning} + +Normalization and preconditioning solve different problems. + +The normalization operator gives each field and residual a physically +meaningful inner product and scale. Conceptually, it maps the raw root system +to coordinates where comparisons such as residual norms and line-search +acceptance are not dominated merely by units. A pressure-like row near +$10^{16}$ and a dimensionless row near unity should not be compared as raw +numbers. + +The preconditioner approximates the inverse action of the scaled Jacobian. It +is concerned with coupling and spectral difficulty, not just magnitude. A good +normalization can improve the numerical setting in which the preconditioner +operates, but it cannot replace an approximation to elliptic, material, +surface, or global-border couplings. + +For a new generated scalar, the specification must provide normalization +metadata that is dimensionally coherent with its target, coordinate, and +residual quantities. For a new distributed thermal field, the discretization +must select a compatible topology and physical scale. If no scale law exists, +the correct outcome is a compile-time rejection of normalized discretization, +not an identity scale inserted without documentation. + +Physics tests for a new normalization should include: + +\begin{itemize} + \item invariance under the intended change of physical units; + \item expected mesh-refinement behavior of the discrete norm; + \item exact results for constant or low-order fields when available; + \item consistent scaling of a residual and its Jacobian action; and + \item quantified interaction with the chosen preconditioner. +\end{itemize} + +\section{A practical development workflow} + +\begin{enumerate} + \item \textbf{Write the equations first.} List state variables, parameters, + outputs, domains, held-fixed variables, and expected limits. + \item \textbf{Reuse or define quantity types.} Avoid raw scalars at public + physics boundaries. + \item \textbf{Declare the smallest honest relation catalog.} Do not promise + an inverse relation that is multivalued or requires an undocumented + closure. + \item \textbf{Implement evaluations and analytic derivatives together.} + Keeping them adjacent makes sign and factor audits easier. + \item \textbf{Add compile-time assertions.} Check the EOS concept, each + required derivative, the exact composed model type, and negative + capability cases. + \item \textbf{Add hand-calculated physics tests.} Use states that expose + every coefficient and term. + \item \textbf{Add property tests.} Check scaling laws, limits, monotonicity, + conservation identities, symmetry, or convexity as appropriate. + \item \textbf{Compare derivatives numerically.} Cover the relevant state + domain, not just one comfortable point. + \item \textbf{Compose with several existing specifications.} Confirm that + the model infers roles and preserves exact types without a bespoke + combination. + \item \textbf{Ask the runtime capability question explicitly.} If false, + document which physical equation or provider is absent. + \item \textbf{Build only the extension target while iterating.} Run the + wider regression suite only when shared library code changes. +\end{enumerate} + +\section{Build and run} + +From the repository root, use the configured build directory and request only +the extension targets: + +\begin{lstlisting}[style=meanfieldshell] +cmake --build cmake-build-profile-homebrew-llvm \ + --target extension_example_demo extension_example_tests +\end{lstlisting} + +Run only the extension tests: + +\begin{lstlisting}[style=meanfieldshell] +./cmake-build-profile-homebrew-llvm/extension_example/extension_example_tests +\end{lstlisting} + +Run the demonstration: + +\begin{lstlisting}[style=meanfieldshell] +./cmake-build-profile-homebrew-llvm/extension_example/extension_example_demo +\end{lstlisting} + +Compile this manual into the build directory: + +\begin{lstlisting}[style=meanfieldshell] +cmake --build cmake-build-profile-homebrew-llvm \ + --target extension_example_manual +\end{lstlisting} + +The extension tests use a separate Catch2 executable. Running it does not run +the main \MeanField{} test suite. + +\section{Gotchas} + +\begin{enumerate} + \item \textbf{A typed scalar is not a unit library.} A + \code{TemperatureValue} says ``temperature,'' not ``kelvin.'' Keep + the whole parameter set in one coherent unit system. + \item \textbf{Relation input order is part of the type.} + $P(\rho,T)$ and $P(T,\rho)$ are different relation types even if a + human could reorder the arguments. + \item \textbf{The catalog is authoritative.} An overload omitted from + \code{Relations} is invisible to generic dispatch. A catalog entry + without an exact overload invalidates the EOS concept. + \item \textbf{State what is held fixed.} A total derivative along an + isentrope is not the same as a partial derivative at fixed + temperature. + \item \textbf{Do not invent an inverse closure.} A two-variable EOS does + not generally supply $\rho(h)$ without another thermodynamic + condition. + \item \textbf{Specification success is not runtime success.} A valid + \code{StellarModel} type can be rejected by \code{discretize} when a + required numerical provider is absent. + \item \textbf{A surface statement must match the formulation.} Fixed + pressure alone does not set both density and temperature at a + thermal surface. + \item \textbf{Stable names are identities.} Keep them unique within a role + and stable across harmless refactors. + \item \textbf{An invariant is not a phase condition.} Fixed total mass is + physically informative; fixed central density may be used to select + a phase. Their generated coordinates have different meanings. + \item \textbf{Analytic formulas still need numerical audits.} Templates can + prove that $\partial P/\partial T$ exists, not that a factor of four + is correct. + \item \textbf{Avoid EOS-specific solver branches.} Extend generic relation, + equation, normalization, and provider mechanisms so future EOS types + follow the same path. + \item \textbf{Validate before powers or division.} Nonfinite inputs and + invalid density should fail with an EOS-domain error rather than + propagate a NaN through a global residual. +\end{enumerate} + +\section{Review checklists} + +\subsection{EOS author checklist} + +\begin{itemize} + \item[$\square$] Assumptions and validity domain are written down. + \item[$\square$] Parameters have physical names and coherent units. + \item[$\square$] Public inputs and outputs use quantity types. + \item[$\square$] The relation catalog is minimal and complete. + \item[$\square$] Every catalog relation has an exact evaluator. + \item[$\square$] Every required Jacobian partial has the correct held-fixed + variable and return type. + \item[$\square$] Nonfinite and out-of-domain states are rejected. + \item[$\square$] Hand-worked values test all coefficients. + \item[$\square$] Physical scaling laws and limiting regimes are tested. + \item[$\square$] Analytic derivatives agree with numerical derivatives. +\end{itemize} + +\subsection{Coupled-model checklist} + +\begin{itemize} + \item[$\square$] Exactly one constitutive law is present. + \item[$\square$] Surface and EOS relations are thermodynamically compatible. + \item[$\square$] Every supplied invariant and phase condition appears in + the inferred model type. + \item[$\square$] Generated unknown and residual arities are square. + \item[$\square$] All Jacobian incidences required by declared dependencies + and effects are present. + \item[$\square$] Every new coordinate and residual has normalization + metadata and a runtime action. + \item[$\square$] The preconditioner includes the important new couplings. + \item[$\square$] Runtime capability is asserted before discretization. + \item[$\square$] Failure of an unsupported combination occurs at compile + time with a useful boundary and message. +\end{itemize} + +\clearpage +\section{Glossary} + +\begin{longtable}{p{0.24\textwidth} p{0.69\textwidth}} +\toprule +\textbf{Term} & \textbf{Meaning in this code base} \\ +\midrule +\endhead +Barotropic EOS & An EOS whose thermodynamic closure can be expressed with one +independent material coordinate for the current problem, such as +$\rho=\rho(h)$. \\ +\addlinespace +Boundary condition & A physical statement applied at a domain boundary, such +as fixed surface pressure. It is a model specification role. \\ +\addlinespace +Canonical specification set & The order-independent compile-time collection of +the exact specification types supplied to \code{StellarModel}. \\ +\addlinespace +Compile-time contract & A property checked by C++ type formation or a concept, +before a numerical run begins. \\ +\addlinespace +Constitutive law & A specification that relates material state quantities. An +EOS is the principal example. \\ +\addlinespace +Discretization & The mesh, finite-element spaces, ordering, normalization +prescription, and runtime data needed to turn a compiled physical system into +a finite-dimensional root problem. \\ +\addlinespace +Equation of state (EOS) & A set of thermodynamic relations with a declared +validity domain and parameters. \\ +\addlinespace +Generated coordinate & A scalar unknown introduced by a specification, such as +the mass-constraint multiplier or angular velocity associated with fixed +angular momentum. \\ +\addlinespace +Invariant & A physically prescribed global quantity enforced by a residual, +such as total mass or total angular momentum. \\ +\addlinespace +Jacobian & The derivative of the full residual vector with respect to the full +state vector. Its block structure is inferred from compiled equations and +specification contributions. \\ +\addlinespace +Model specification & One exact physical choice carrying a role, stable key, +parameters, and structural contribution metadata. \\ +\addlinespace +Normalization & A physically informed mapping that supplies comparable +coordinates and residual norms across fields with different units, topologies, +and magnitudes. \\ +\addlinespace +Partial derivative & A derivative with respect to one declared relation input +while its other inputs are held fixed. \\ +\addlinespace +Phase condition & A condition that removes a neutral direction or chooses one +representative from a family of equivalent solutions. It is distinct from a +physical invariant. \\ +\addlinespace +Physical Riesz map & The discrete map induced by a selected physical inner +product and scale. It connects a field or residual with its dual coordinate in +a topology-aware way. \\ +\addlinespace +Preconditioner & An efficient approximation to the inverse scaled Jacobian, +used to make the linearized solve tractable. \\ +\addlinespace +Quantity type & A zero-storage type that names physical meaning, such as +\code{Temperature}. A \code{QuantityValue} stores the scalar. \\ +\addlinespace +Relation catalog & The complete compile-time list of input-output relations an +EOS claims to implement. \\ +\addlinespace +Residual & One equation written as a quantity that should be zero at a +solution. The full nonlinear problem is a vector of residual blocks. \\ +\addlinespace +Runtime provider & Numerical code that evaluates or assembles an inferred +physics contribution for a particular formulation. \\ +\addlinespace +Solver border & The small set of generated global scalar rows and columns +coupled to the distributed physical core. \\ +\addlinespace +Specification role & The category of a physical choice: constitutive law, +boundary condition, invariant, phase condition, gauge choice, or rotation law. \\ +\addlinespace +Stable name & A compile-time string used with a role to identify one +specification mechanism consistently across inferred structures and runtime +descriptions. \\ +\addlinespace +Stellar model & The strongly typed, canonical composition of the exact physical +specification objects selected by a user. \\ +\addlinespace +Symbolically square & The compiled state and residual descriptions have equal +total scalar arity before mesh-dependent sizes are known. \\ +\addlinespace +Thermal closure & The additional equation or prescription needed to determine +an independent thermal variable such as temperature or entropy. \\ +\addlinespace +Typed value & A scalar wrapper whose C++ type carries its physical meaning and +prevents accidental interchange of unrelated quantities. \\ +\bottomrule +\end{longtable} + +\section{Final perspective} + +The extension mechanism is successful when a physics developer can read the +new source primarily as equations, parameters, domains, and derivatives. The +compiler should then answer structural questions: whether the relations are +complete, whether the selected model is square, which exact constraints are +present, and whether the current numerical backend implements every inferred +piece. + +The ideal-gas plus radiation example intentionally reaches that boundary. It +demonstrates a valid nonbarotropic EOS and a valid rotating stellar-model +specification without claiming a nonexistent thermal equilibrium solve. That +is the behavior future extensions should preserve: composable when supported, +precisely rejected when incomplete, and always honest about the physics. + +\end{document} diff --git a/extension_example/rotating_stellar_model.cppm b/extension_example/rotating_stellar_model.cppm new file mode 100644 index 0000000..efb9512 --- /dev/null +++ b/extension_example/rotating_stellar_model.cppm @@ -0,0 +1,61 @@ +module; + +#include +#include + +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 rotationAxis{0.0, 0.0, 1.0}; + std::array rotationCenter{0.0, 0.0, 0.0}; + }; + + [[nodiscard]] auto makeRotatingStellarModel(const RotatingStellarModelParameters ¶meters) { + return mean_field::model::StellarModel( + IdealGasRadiation(parameters.equationOfState), + mean_field::surface::Isobaric({.Psurf = parameters.surfacePressure}), + mean_field::integral::FixedTotalMass({.Mtotal = parameters.totalMass}), + mean_field::integral::FixedAngularMomentum({ + .Jtotal = parameters.totalAngularMomentum, + .axis = parameters.rotationAxis, + .center = parameters.rotationCenter + }) + ); + } + + using RotatingStellarModel = decltype( + makeRotatingStellarModel(std::declval()) + ); + + static_assert(mean_field::model::StellarModelType); + 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; + + static_assert(!currentEquilibriumBackendSupportsIdealGasRadiation); +} // namespace mean_field::extension_example diff --git a/extension_example/tests/ideal_gas_radiation.cpp b/extension_example/tests/ideal_gas_radiation.cpp new file mode 100644 index 0000000..fef293b --- /dev/null +++ b/extension_example/tests/ideal_gas_radiation.cpp @@ -0,0 +1,292 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +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 + [[nodiscard]] double centeredDifference( + Function function, + const double point + ) { + const double step = std::cbrt(std::numeric_limits::epsilon()) * + std::max(1.0, std::abs(point)); + return (function(point + step) - function(point - step)) / (2.0 * step); + } + + template + concept CanEvaluatePressureWithReversedInputs = requires( + const EquationOfState &equationOfState, + const dimensions::TemperatureValue temperature, + const dimensions::DensityValue density + ) { + eos::evaluate(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); + STATIC_CHECK(eos::EquationOfStateModel); + STATIC_CHECK(eos::SupportsRelation); + STATIC_CHECK(eos::SupportsRelation); + STATIC_CHECK(eos::SupportsRelation); + STATIC_CHECK_FALSE(eos::BarotropicClosureEquationOfState); + STATIC_CHECK_FALSE(CanEvaluatePressureWithReversedInputs); + + using PressureResult = decltype(eos::evaluate( + std::declval(), + dimensions::DensityValue{1.0}, + dimensions::TemperatureValue{1.0} + )); + STATIC_CHECK(std::same_as); +} + +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( + equationOfState, + density, + temperature + ); + const auto internalEnergy = eos::evaluate( + equationOfState, + density, + temperature + ); + const auto enthalpy = eos::evaluate( + 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( + equationOfState, + dimensions::DensityValue{rho}, + temperature + ).value(); + }, + state.density + ); + const double numericalPressureTemperature = centeredDifference( + [&](const double T) { + return eos::evaluate( + equationOfState, + density, + dimensions::TemperatureValue{T} + ).value(); + }, + state.temperature + ); + const double numericalEnergyDensity = centeredDifference( + [&](const double rho) { + return eos::evaluate( + equationOfState, + dimensions::DensityValue{rho}, + temperature + ).value(); + }, + state.density + ); + const double numericalEnergyTemperature = centeredDifference( + [&](const double T) { + return eos::evaluate( + equationOfState, + density, + dimensions::TemperatureValue{T} + ).value(); + }, + state.temperature + ); + const double numericalEnthalpyDensity = centeredDifference( + [&](const double rho) { + return eos::evaluate( + equationOfState, + dimensions::DensityValue{rho}, + temperature + ).value(); + }, + state.density + ); + const double numericalEnthalpyTemperature = centeredDifference( + [&](const double T) { + return eos::evaluate( + 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::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( + equationOfState, + dimensions::DensityValue{0.0}, + dimensions::TemperatureValue{1.0} + ), + eos::EvaluationError + ); + CHECK_THROWS_AS( + eos::evaluate( + equationOfState, + dimensions::DensityValue{1.0}, + dimensions::TemperatureValue{-1.0} + ), + eos::EvaluationError + ); + CHECK_THROWS_AS( + eos::evaluate( + equationOfState, + dimensions::DensityValue{nan}, + dimensions::TemperatureValue{1.0} + ), + eos::EvaluationError + ); +} diff --git a/extension_example/tests/rotating_stellar_model.cpp b/extension_example/tests/rotating_stellar_model.cpp new file mode 100644 index 0000000..f0527a5 --- /dev/null +++ b/extension_example/tests/rotating_stellar_model.cpp @@ -0,0 +1,67 @@ +#include +#include + +#include + +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; + + STATIC_CHECK(std::same_as); + STATIC_CHECK(model::StellarModelType); + STATIC_CHECK(Model::symbolicallySquare); + STATIC_CHECK(Model::specificationCount == 4); + STATIC_CHECK(std::same_as, example::IdealGasRadiation>); + STATIC_CHECK(Model::template containsSpecification); + STATIC_CHECK(Model::template containsSpecification); + STATIC_CHECK(Model::template specificationRoleCount == 1); + STATIC_CHECK(Model::template specificationRoleCount == 1); + STATIC_CHECK(Model::template specificationRoleCount == 2); + + CHECK(stellarModel.equationOfState().parameters().meanMolecularWeight == 0.62); + CHECK(stellarModel.surfaceCondition().targetPressure() == dimensions::PressureValue{0.0}); + CHECK(stellarModel.specification().targetMass() == dimensions::MassValue{1.75}); + + const auto &angularMomentum = stellarModel.specification(); + 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); + STATIC_CHECK_FALSE(mean_field::extension_example::currentEquilibriumBackendSupportsIdealGasRadiation); + STATIC_CHECK_FALSE(mean_field::equilibrium::StellarEquilibriumModel); +} diff --git a/libmeanfield/impl/operators/prepared_angular_momentum.cpp b/libmeanfield/impl/operators/prepared_angular_momentum.cpp new file mode 100644 index 0000000..8354d05 --- /dev/null +++ b/libmeanfield/impl/operators/prepared_angular_momentum.cpp @@ -0,0 +1,590 @@ +module; + +#include +#include +#include +#include + +#include + +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(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; + 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::quadrature::QuadratureRole::discretization, + transformation.OrderW(), + std::array{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 ¢er = 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(component)]; + radiusSquared += relative * relative; + axialPosition += axis[static_cast(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 ¢er = 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(component)]; + relativeDotVariation += relative * physicalPositionVariation(component); + axialPosition += axis[static_cast(component)] * relative; + axialVariation += axis[static_cast(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 diff --git a/libmeanfield/impl/operators/prepared_central_density_stellar_equilibrium.cpp b/libmeanfield/impl/operators/prepared_central_density_stellar_equilibrium.cpp deleted file mode 100644 index 1e19088..0000000 --- a/libmeanfield/impl/operators/prepared_central_density_stellar_equilibrium.cpp +++ /dev/null @@ -1,223 +0,0 @@ -module; - -#include -#include -#include -#include -#include - -#include - -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 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 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(*f.enthalpyFes); - mfem::Vector origin(f.mesh->SpaceDimension()); - origin = 0.0; - return field::make_field_point_dof_map(*f.enthalpyFes, enthalpyMap, origin, 1.0e-12); - } - - PreparedCentralDensityStellarEquilibriumOperator::PreparedCentralDensityStellarEquilibriumOperator( - fem::FEM &f, - std::unique_ptr 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(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(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 diff --git a/libmeanfield/impl/operators/prepared_hydrostatic_equilibrium.cpp b/libmeanfield/impl/operators/prepared_hydrostatic_equilibrium.cpp index 10eb935..8ba7a78 100644 --- a/libmeanfield/impl/operators/prepared_hydrostatic_equilibrium.cpp +++ b/libmeanfield/impl/operators/prepared_hydrostatic_equilibrium.cpp @@ -903,6 +903,47 @@ namespace mean_field::operators { ++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( const mfem::Vector &enthalpyVariation, const mfem::Vector &gravityPotentialVariation, diff --git a/libmeanfield/impl/operators/prepared_stellar_equilibrium.cpp b/libmeanfield/impl/operators/prepared_stellar_equilibrium.cpp index 1e57f4c..fafb13d 100644 --- a/libmeanfield/impl/operators/prepared_stellar_equilibrium.cpp +++ b/libmeanfield/impl/operators/prepared_stellar_equilibrium.cpp @@ -356,8 +356,11 @@ namespace mean_field::operators { m_rootManifest( constructionData.valueSizes, constructionData.residualSizes, - fixedMassConstraint.targetMass().value(), - surfaceConstraint.descriptor().targetPressure, + StellarEquilibriumSpecificationModel{ + equationOfState, + surface::Isobaric{ + dimensions::PressureValue{surfaceConstraint.descriptor().targetPressure}}, + fixedMassConstraint.specification()}, constructionData.pressureSurfaceRows.size() ), m_gravityStateOffsets(constructionData.gravityStateOffsets), @@ -431,6 +434,7 @@ namespace mean_field::operators { m_fullMechanicalAction.SetSize(m_domainDeformation.volumeDisplacementSize()); m_surfaceShapeAction.SetSize(m_domainDeformation.parameterCount()); m_pullbackDerivativeAction.SetSize(m_domainDeformation.parameterCount()); + m_densityVolumeIntegralAction.SetSize(1); m_gravityState = 0.0; m_gravityDirection = 0.0; @@ -441,6 +445,7 @@ namespace mean_field::operators { m_fullMechanicalAction = 0.0; m_surfaceShapeAction = 0.0; m_pullbackDerivativeAction = 0.0; + m_densityVolumeIntegralAction = 0.0; } PreparedStellarEquilibriumReport PreparedStellarEquilibriumOperator::Prepare( @@ -494,13 +499,13 @@ namespace mean_field::operators { const auto rootState = m_rootManifest.stateView(state); - const mfem::Vector reducedDensity = rootState.block(utils::blocks::density_field.mass_term); - const mfem::Vector surfaceDeformationParameters = + const auto reducedDensity = rootState.block(utils::blocks::density_field.mass_term); + const auto surfaceDeformationParameters = rootState.block(utils::blocks::surface_deformation_field.parameters_term); - const mfem::Vector gravityGradient = rootState.block(utils::blocks::gravity_field.gradient_term); - const mfem::Vector gravityPotential = rootState.block(utils::blocks::gravity_field.poisson_term); - const mfem::Vector reducedEnthalpy = rootState.block(utils::blocks::enthalpy_field.specific_term); - const mfem::Vector bernoulli = + const auto gravityGradient = rootState.block(utils::blocks::gravity_field.gradient_term); + const auto gravityPotential = rootState.block(utils::blocks::gravity_field.poisson_term); + const auto reducedEnthalpy = rootState.block(utils::blocks::enthalpy_field.specific_term); + const auto bernoulli = rootState.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term); const bool generatedGeometryChanged = @@ -633,13 +638,13 @@ namespace mean_field::operators { const auto rootDirection = m_rootManifest.directionView(direction); - const mfem::Vector reducedDensityDirection = rootDirection.block(utils::blocks::density_field.mass_term); - const mfem::Vector surfaceDeformationDirection = + const auto reducedDensityDirection = rootDirection.block(utils::blocks::density_field.mass_term); + const auto surfaceDeformationDirection = rootDirection.block(utils::blocks::surface_deformation_field.parameters_term); - const mfem::Vector gravityGradientDirection = rootDirection.block(utils::blocks::gravity_field.gradient_term); - const mfem::Vector gravityPotentialDirection = rootDirection.block(utils::blocks::gravity_field.poisson_term); - const mfem::Vector reducedEnthalpyDirection = rootDirection.block(utils::blocks::enthalpy_field.specific_term); - const mfem::Vector bernoulliDirection = + const auto gravityGradientDirection = rootDirection.block(utils::blocks::gravity_field.gradient_term); + const auto gravityPotentialDirection = rootDirection.block(utils::blocks::gravity_field.poisson_term); + const auto reducedEnthalpyDirection = rootDirection.block(utils::blocks::enthalpy_field.specific_term); + const auto bernoulliDirection = rootDirection.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term); m_domainDeformation.applyJacobian( @@ -794,6 +799,41 @@ namespace mean_field::operators { 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 & PreparedStellarEquilibriumOperator::GetSurfaceConstraintOperator() const noexcept { return m_surfaceConstraintOperator; diff --git a/libmeanfield/impl/seed/stellar_equilibrium_projection.cpp b/libmeanfield/impl/seed/stellar_equilibrium_projection.cpp index e0e80b1..66aa17a 100644 --- a/libmeanfield/impl/seed/stellar_equilibrium_projection.cpp +++ b/libmeanfield/impl/seed/stellar_equilibrium_projection.cpp @@ -3,6 +3,7 @@ module; #include #include #include +#include #include #include @@ -133,21 +134,15 @@ namespace { namespace mean_field::seed::detail { ProjectedRadialFields projectRadialFields( - const equilibrium::StellarDiscretization &discretization, + fem::FEM &finiteElementModel, const RadialProfile &profile, const dimensions::MassValue targetMass, - const dimensions::PressureValue targetSurfacePressure, const StellarEquilibriumProjectionOptions &options ) { validate_profile(profile); if (!std::isfinite(options.surfaceRadiusRelativeTolerance) || options.surfaceRadiusRelativeTolerance < 0.0) { 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 double targetRadius = profile.stellarRadius.value(); const double comparisonScale = std::max({targetRadius, surfaceRadius.maximum, 1.0e-300}); @@ -185,6 +180,20 @@ namespace mean_field::seed::detail { const physics::GravitySolution gravitySolution = 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 = field::make_field_dof_grid_function_adapter(*finiteElementModel.densityFes); const field::FieldDofGridFunctionAdapter enthalpyAdapter = @@ -203,7 +212,8 @@ namespace mean_field::seed::detail { .gravityGradient = gravityFluxAdapter.gather(gravitySolution.gradPhi), .gravityPotential = gravityPotentialAdapter.gather(gravitySolution.phi), .specificEnthalpy = enthalpyAdapter.gather(enthalpyField), - .bernoulliConstant = -utils::G * targetMass.value() / targetRadius + .bernoulliConstant = -utils::G * targetMass.value() / targetRadius, + .sphericalMomentOfInertia = sphericalMomentOfInertia }; } } // namespace mean_field::seed::detail diff --git a/libmeanfield/interface/equilibrium/stellar_discretization.cppm b/libmeanfield/interface/equilibrium/stellar_discretization.cppm index 0bc0e64..534b3a7 100644 --- a/libmeanfield/interface/equilibrium/stellar_discretization.cppm +++ b/libmeanfield/interface/equilibrium/stellar_discretization.cppm @@ -1,12 +1,16 @@ module; +#include #include #include +#include +#include export module mean_field:equilibrium.stellar_discretization; export import :fem; export import :mapping.domain_mapper; +export import :normalization.physical_riesz; export namespace mean_field::equilibrium { /* @@ -18,26 +22,78 @@ export namespace mean_field::equilibrium { * mutable field workspaces. Separating those workspaces is a prerequisite * for shared discretization ownership by solved Structure objects. */ - class StellarDiscretization final { + template + class StellarDiscretizationFor final { public: - explicit StellarDiscretization(fem::FEM &finiteElementModel) - : StellarDiscretization( + using NormalizationPrescriptionType = std::remove_cvref_t; + + explicit StellarDiscretizationFor(fem::FEM &finiteElementModel) + requires std::same_as + : StellarDiscretizationFor( finiteElementModel, - RequireDomainMapper(finiteElementModel) + RequireDomainMapper(finiteElementModel), + normalization::Unnormalized{} ) { } - StellarDiscretization( + StellarDiscretizationFor( fem::FEM &finiteElementModel, const mapping::DomainMapper &domainMapper + ) + requires std::same_as + : StellarDiscretizationFor( + finiteElementModel, + domainMapper, + normalization::Unnormalized{} + ) { + } + + StellarDiscretizationFor( + fem::FEM &, + mapping::DomainMapper && + ) requires std::same_as = delete; + + StellarDiscretizationFor( + fem::FEM &, + const mapping::DomainMapper && + ) requires std::same_as = 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_domainMapper(std::addressof(domainMapper)) { + m_domainMapper(std::addressof(domainMapper)), + m_normalizationPrescription(std::move(normalizationPrescription)) { if (!finiteElementModel.okay()) { 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 { return *m_finiteElementModel; } @@ -46,6 +102,10 @@ export namespace mean_field::equilibrium { return *m_domainMapper; } + [[nodiscard]] const NormalizationPrescriptionType &normalizationPrescription() const noexcept { + return m_normalizationPrescription; + } + [[nodiscard]] bool isCurrent() const noexcept { return m_finiteElementModel != nullptr && m_domainMapper != nullptr && m_finiteElementModel->okay(); } @@ -60,5 +120,64 @@ export namespace mean_field::equilibrium { fem::FEM *m_finiteElementModel; const mapping::DomainMapper *m_domainMapper; + NormalizationPrescriptionType m_normalizationPrescription; }; + + template + StellarDiscretizationFor(fem::FEM &, Normalization) + -> StellarDiscretizationFor>; + + template + StellarDiscretizationFor(fem::FEM &, const mapping::DomainMapper &, Normalization) + -> StellarDiscretizationFor>; + + using StellarDiscretization = StellarDiscretizationFor; + + template struct IsStellarDiscretization : std::false_type { }; + + template + struct IsStellarDiscretization> : std::true_type { }; + + template + concept StellarDiscretizationType = IsStellarDiscretization>::value; + + template + [[nodiscard]] auto makeStellarDiscretization( + fem::FEM &finiteElementModel, + Normalization normalizationPrescription + ) { + return StellarDiscretizationFor>{ + finiteElementModel, + std::move(normalizationPrescription) + }; + } + + template + [[nodiscard]] auto makeStellarDiscretization( + fem::FEM &finiteElementModel, + const mapping::DomainMapper &domainMapper, + Normalization normalizationPrescription + ) { + return StellarDiscretizationFor>{ + finiteElementModel, + domainMapper, + std::move(normalizationPrescription) + }; + } + + template + StellarDiscretizationFor> + makeStellarDiscretization( + fem::FEM &, + mapping::DomainMapper &&, + Normalization + ) = delete; + + template + StellarDiscretizationFor> + makeStellarDiscretization( + fem::FEM &, + const mapping::DomainMapper &&, + Normalization + ) = delete; } // namespace mean_field::equilibrium diff --git a/libmeanfield/interface/field/field_registry.cppm b/libmeanfield/interface/field/field_registry.cppm index d73417b..21eca9a 100644 --- a/libmeanfield/interface/field/field_registry.cppm +++ b/libmeanfield/interface/field/field_registry.cppm @@ -231,6 +231,28 @@ export namespace mean_field::field { 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; + 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 // non-spatial numerical coordinate rather than a physical stellar field. struct CentralDensityBorder { diff --git a/libmeanfield/interface/mean_field.cppm b/libmeanfield/interface/mean_field.cppm index ae9d288..c2a042d 100644 --- a/libmeanfield/interface/mean_field.cppm +++ b/libmeanfield/interface/mean_field.cppm @@ -27,6 +27,7 @@ export import :quadrature.mfem; export import :solver.fields; export import :solver.preconditioning_diagnostics; export import :preconditioning; +export import :normalization; export import :utils.blocks; export import :operators.gravity_field; export import :operators.gravity_field_jacobian; @@ -60,6 +61,7 @@ export import :model.structure.polytropic; export import :model.specifications; export import :model.typed_stellar; export import :model.compiled_fixed_mass; +export import :model.compiled_fixed_angular_momentum; export import :model.compiled_fixed_central_density; export import :eos.quantities; export import :eos.relations; @@ -85,11 +87,13 @@ export import :model.stellar; export import :operators.root_manifest; export import :operators.prepared_constraint; export import :operators.prepared_mass_normalization; +export import :operators.prepared_angular_momentum; export import :operators.prepared_central_density; export import :operators.prepared_centering_constraint; export import :operators.prepared_surface_constraint; 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 :operators.stellar_equilibrium_problem; export import :seed.stellar_equilibrium_projection; diff --git a/libmeanfield/interface/models/compiled_fixed_angular_momentum.cppm b/libmeanfield/interface/models/compiled_fixed_angular_momentum.cppm new file mode 100644 index 0000000..790befd --- /dev/null +++ b/libmeanfield/interface/models/compiled_fixed_angular_momentum.cppm @@ -0,0 +1,74 @@ +module; + +#include +#include +#include + +#include + +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, + ResidualFor, + 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(component)]; + center(component) = m_specification.center()[static_cast(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); + static_assert(CompiledConstraint); +} // namespace mean_field::models diff --git a/libmeanfield/interface/models/specifications.cppm b/libmeanfield/interface/models/specifications.cppm index 541fc7d..975e612 100644 --- a/libmeanfield/interface/models/specifications.cppm +++ b/libmeanfield/interface/models/specifications.cppm @@ -28,9 +28,450 @@ export namespace mean_field::models { rotation_law }; + /* + * The small declarations in this section are the physics-facing model + * extension API. A specification owns one nested ModelDefinition and the + * compiler projects the lower-level traits from it. Extension authors do + * not specialize a registry or choose a globally coordinated ordinal. + */ + template struct FixedString final { + char characters[Extent]{}; + + consteval FixedString(const char (&text)[Extent]) noexcept { + for (std::size_t index = 0; index < Extent; ++index) { + characters[index] = text[index]; + } + } + + [[nodiscard]] constexpr std::string_view view() const noexcept { + static_assert(Extent > 0); + return {characters, Extent - 1}; + } + + constexpr bool operator==(const FixedString &) const = default; + }; + + template FixedString(const char (&)[Extent]) -> FixedString; + + template struct ModelTypeList final { + static constexpr std::size_t size = sizeof...(Types); + }; + + template + using DependsOn = ModelTypeList; + + template + using Affects = ModelTypeList; + + /* + * Physics vocabulary for declaring how a stellar specification couples to + * the equilibrium system. These names deliberately do not import the + * solver's block registry: the operator compiler translates them once at + * its backend boundary. Advanced extensions may still place an existing + * backend block type directly in DependsOn/Affects. + */ + namespace stellar { + namespace state { + struct Density final { }; + struct SurfaceShape final { }; + struct GravityGradient final { }; + struct GravitationalPotential final { }; + struct SpecificEnthalpy final { }; + + /* + * A coordinate generated by another named specification. This is + * the physics-facing spelling for coupled global constraints: an + * extension names the constraint it reads, never its solver block. + */ + template + struct GeneratedCoordinateOf final { + using SpecificationType = Specification; + }; + + /* + * The coordinate generated by the specification containing this + * marker. For example, FixedAngularMomentum uses it to state that + * its integral residual depends on angular velocity without naming + * a generated solver block. + */ + struct OwnGeneratedCoordinate final { }; + } // namespace state + + namespace equation { + struct GravityGradientDefinition final { }; + struct PoissonEquation final { }; + struct DensityClosure final { }; + struct SurfaceShapeBalance final { }; + struct HydrostaticBalance final { }; + + /* The scalar constraint equation owned by this specification. */ + struct OwnConstraint final { }; + + /* The scalar constraint equation owned by another specification. */ + template + struct ConstraintOf final { + using SpecificationType = Specification; + }; + } // namespace equation + + /* + * A readable, compile-time label for one declared Jacobian derivative. + * Runtime providers consume this vocabulary without learning backend + * row and column block types. + */ + template + struct Derivative final { + using EquationType = Equation; + using StateType = State; + }; + } // namespace stellar + + enum class GeneratedStateKind { none, multiplier, physical_coordinate, solver_border }; + + enum class RieszTopology { + unavailable, + identity, + scalar_volume_l2, + vector_volume_l2, + scalar_boundary_l2, + hybrid_scalar_volume_point_rows, + global_scalar + }; + + enum class PhysicalScaleLaw { + unavailable, + dimensionless, + density, + length, + acceleration, + inverse_time_squared, + specific_energy, + pressure, + mass, + force, + angular_velocity, + angular_momentum + }; + + /* + * PhysicalScaleLaw is the numerical scaling vocabulary, while the + * dimensions module carries the authoritative semantic quantity types. + * Keep their relationship in one extensible trait so a physics-facing + * scalar declaration cannot spell a quantity in one place and an + * unrelated normalization scale somewhere else. + */ + template struct PhysicalScaleForQuantity { + static constexpr PhysicalScaleLaw value = PhysicalScaleLaw::unavailable; + }; + + template <> struct PhysicalScaleForQuantity { + static constexpr PhysicalScaleLaw value = PhysicalScaleLaw::dimensionless; + }; + + template <> struct PhysicalScaleForQuantity { + static constexpr PhysicalScaleLaw value = PhysicalScaleLaw::mass; + }; + + template <> struct PhysicalScaleForQuantity { + static constexpr PhysicalScaleLaw value = PhysicalScaleLaw::length; + }; + + template <> struct PhysicalScaleForQuantity { + static constexpr PhysicalScaleLaw value = PhysicalScaleLaw::density; + }; + + template <> struct PhysicalScaleForQuantity { + static constexpr PhysicalScaleLaw value = PhysicalScaleLaw::acceleration; + }; + + template <> struct PhysicalScaleForQuantity { + static constexpr PhysicalScaleLaw value = PhysicalScaleLaw::specific_energy; + }; + + template <> struct PhysicalScaleForQuantity { + static constexpr PhysicalScaleLaw value = PhysicalScaleLaw::specific_energy; + }; + + template <> struct PhysicalScaleForQuantity { + static constexpr PhysicalScaleLaw value = PhysicalScaleLaw::specific_energy; + }; + + template <> struct PhysicalScaleForQuantity { + static constexpr PhysicalScaleLaw value = PhysicalScaleLaw::pressure; + }; + + template <> struct PhysicalScaleForQuantity { + static constexpr PhysicalScaleLaw value = PhysicalScaleLaw::force; + }; + + template <> struct PhysicalScaleForQuantity { + static constexpr PhysicalScaleLaw value = PhysicalScaleLaw::angular_velocity; + }; + + template <> struct PhysicalScaleForQuantity { + static constexpr PhysicalScaleLaw value = PhysicalScaleLaw::angular_momentum; + }; + + template + inline constexpr PhysicalScaleLaw physicalScaleForQuantity = + PhysicalScaleForQuantity>::value; + + template + concept PhysicalScaleRepresentedQuantity = + dimensions::PhysicalQuantityType && + physicalScaleForQuantity != PhysicalScaleLaw::unavailable && + requires { + typename std::bool_constant< + !static_cast( + Quantity::identifier).empty()>; + }; + + namespace detail { + template [[nodiscard]] consteval bool declaredCoordinateNormalizationIsAvailable() { + if constexpr (requires { + { Candidate::available } -> std::convertible_to; + }) { + return static_cast(Candidate::available); + } else { + return false; + } + } + + [[nodiscard]] consteval bool isKnownSpecificationRole(const SpecificationRole role) { + switch (role) { + case SpecificationRole::constitutive_law: + case SpecificationRole::boundary_condition: + case SpecificationRole::invariant: + case SpecificationRole::phase_condition: + case SpecificationRole::gauge_choice: + case SpecificationRole::rotation_law: + return true; + } + return false; + } + + [[nodiscard]] consteval bool isKnownGeneratedStateKind(const GeneratedStateKind kind) { + switch (kind) { + case GeneratedStateKind::none: + case GeneratedStateKind::multiplier: + case GeneratedStateKind::physical_coordinate: + case GeneratedStateKind::solver_border: + return true; + } + return false; + } + + [[nodiscard]] consteval bool specificationRoleAcceptsGeneratedStateKind( + const SpecificationRole role, + const GeneratedStateKind kind + ) { + switch (role) { + case SpecificationRole::constitutive_law: + case SpecificationRole::boundary_condition: + return kind == GeneratedStateKind::none; + case SpecificationRole::invariant: + return kind == GeneratedStateKind::multiplier || + kind == GeneratedStateKind::physical_coordinate; + case SpecificationRole::phase_condition: + case SpecificationRole::gauge_choice: + return kind == GeneratedStateKind::solver_border; + case SpecificationRole::rotation_law: + /* + * Rotation laws currently prescribe a physical profile; they + * do not own a root coordinate. Fixed angular momentum owns + * angular velocity as an invariant/physical-coordinate pair. + * Keep this closed until a state-generating rotation-law + * contract is designed and implemented end to end. + */ + return kind == GeneratedStateKind::none; + } + return false; + } + } // namespace detail + + template + concept CompatibleSpecificationRoleAndGeneratedState = + detail::isKnownSpecificationRole(Role) && detail::isKnownGeneratedStateKind(StateKind) && + detail::specificationRoleAcceptsGeneratedStateKind(Role, StateKind); + + template struct CoordinateNormalization final { + static constexpr RieszTopology topology = Topology; + static constexpr PhysicalScaleLaw scale = Scale; + static constexpr bool available = + topology != RieszTopology::unavailable && scale != PhysicalScaleLaw::unavailable; + }; + + using UnavailableCoordinateNormalization = + CoordinateNormalization; + + template + struct GeneratedNormalization final { + using Value = ValueNormalization; + using Residual = ResidualNormalization; + + static constexpr bool available = detail::declaredCoordinateNormalizationIsAvailable() && + detail::declaredCoordinateNormalizationIsAvailable(); + }; + + using UnavailableGeneratedNormalization = GeneratedNormalization<>; + + template + using GlobalScalarNormalization = GeneratedNormalization< + CoordinateNormalization, + CoordinateNormalization>; + + template + struct GeneratedManifest final { + private: + inline static constexpr auto valueStableIdStorage = ValueStableId; + inline static constexpr auto valueSymbolStorage = ValueSymbol; + inline static constexpr auto residualStableIdStorage = ResidualStableId; + inline static constexpr auto residualSymbolStorage = ResidualSymbol; + inline static constexpr auto targetUnitsStorage = TargetUnits; + inline static constexpr auto residualUnitsStorage = ResidualUnits; + + public: + static constexpr std::string_view valueStableId = valueStableIdStorage.view(); + static constexpr std::string_view valueSymbol = valueSymbolStorage.view(); + static constexpr std::string_view residualStableId = residualStableIdStorage.view(); + static constexpr std::string_view residualSymbol = residualSymbolStorage.view(); + static constexpr std::string_view targetUnits = targetUnitsStorage.view(); + static constexpr std::string_view residualUnits = residualUnitsStorage.view(); + static constexpr bool available = !valueStableId.empty() && !valueSymbol.empty() && !residualStableId.empty() && + !residualSymbol.empty() && !targetUnits.empty() && !residualUnits.empty(); + }; + + using UnavailableGeneratedManifest = GeneratedManifest<>; + + /* + * A scalar constraint has three independent dimensional statements: + * + * - the physical quantity supplied as its target; + * - the generated Newton coordinate; and + * - the appended scalar residual. + * + * They are deliberately not equated. FixedCentralDensity, for example, + * has a density target but a specific-enthalpy phase residual. The + * quantity types below generate both numerical scale laws and diagnostic + * unit labels, making the strings presentation rather than authority. + */ + template < + PhysicalScaleRepresentedQuantity TargetQuantityT, + PhysicalScaleRepresentedQuantity GeneratedCoordinateQuantityT, + PhysicalScaleRepresentedQuantity ConstraintResidualQuantityT, + FixedString ValueStableId, + FixedString ValueSymbol, + FixedString ResidualStableId, + FixedString ResidualSymbol> + struct DimensionalScalarConstraint final { + using TargetQuantity = TargetQuantityT; + using GeneratedCoordinateQuantity = GeneratedCoordinateQuantityT; + using ConstraintResidualQuantity = ConstraintResidualQuantityT; + using TargetValue = dimensions::QuantityValue; + static constexpr PhysicalScaleLaw targetScale = + physicalScaleForQuantity; + + struct Normalization final { + using TargetQuantity = TargetQuantityT; + using GeneratedCoordinateQuantity = GeneratedCoordinateQuantityT; + using ConstraintResidualQuantity = ConstraintResidualQuantityT; + using TargetValue = dimensions::QuantityValue; + static constexpr PhysicalScaleLaw targetScale = + physicalScaleForQuantity; + using Value = CoordinateNormalization< + RieszTopology::global_scalar, + physicalScaleForQuantity>; + using Residual = CoordinateNormalization< + RieszTopology::global_scalar, + physicalScaleForQuantity>; + + static constexpr bool available = Value::available && Residual::available; + }; + + struct Manifest final { + using TargetQuantity = TargetQuantityT; + using GeneratedCoordinateQuantity = GeneratedCoordinateQuantityT; + using ConstraintResidualQuantity = ConstraintResidualQuantityT; + + private: + inline static constexpr auto valueStableIdStorage = ValueStableId; + inline static constexpr auto valueSymbolStorage = ValueSymbol; + inline static constexpr auto residualStableIdStorage = ResidualStableId; + inline static constexpr auto residualSymbolStorage = ResidualSymbol; + + public: + static constexpr std::string_view valueStableId = valueStableIdStorage.view(); + static constexpr std::string_view valueSymbol = valueSymbolStorage.view(); + static constexpr std::string_view residualStableId = residualStableIdStorage.view(); + static constexpr std::string_view residualSymbol = residualSymbolStorage.view(); + static constexpr std::string_view targetUnits = TargetQuantity::identifier; + static constexpr std::string_view residualUnits = ConstraintResidualQuantity::identifier; + static constexpr bool available = + !valueStableId.empty() && !valueSymbol.empty() && + !residualStableId.empty() && !residualSymbol.empty() && + !targetUnits.empty() && !residualUnits.empty(); + }; + + static constexpr bool dimensionallyTyped = true; + }; + + template , + typename AffectedResidualBlocks = ModelTypeList<>, + typename NormalizationDefinition = UnavailableGeneratedNormalization, + typename ManifestDefinition = UnavailableGeneratedManifest> + struct ModelDefinition final { + using SpecificationType = Specification; + using DependsOn = DependsOnBlocks; + using Affects = AffectedResidualBlocks; + using Normalization = NormalizationDefinition; + using Manifest = ManifestDefinition; + + private: + inline static constexpr auto stableNameStorage = StableName; + + public: + static constexpr std::string_view name = stableNameStorage.view(); + static constexpr SpecificationRole role = Role; + static constexpr GeneratedStateKind generatedStateKind = StateKind; + static constexpr std::size_t generatedValueArity = StateKind == GeneratedStateKind::none ? 0U : 1U; + static constexpr std::size_t generatedResidualArity = StateKind == GeneratedStateKind::none ? 0U : 1U; + static constexpr bool structurallyAvailable = !name.empty(); + }; + + template + using ConstitutiveLaw = ModelDefinition; + + template + using BoundaryCondition = ModelDefinition; + + template , + typename Affects = ModelTypeList<>, typename Normalization = UnavailableGeneratedNormalization, + typename Manifest = UnavailableGeneratedManifest> + using FixedIntegralWithMultiplier = + ModelDefinition; + + template , + typename Affects = ModelTypeList<>, typename Normalization = UnavailableGeneratedNormalization, + typename Manifest = UnavailableGeneratedManifest> + using FixedIntegralWithPhysicalCoordinate = + ModelDefinition; + + template , + typename Affects = ModelTypeList<>, typename Normalization = UnavailableGeneratedNormalization, + typename Manifest = UnavailableGeneratedManifest> + using PhaseCondition = + ModelDefinition; + struct SpecificationKey final { SpecificationRole role; - std::size_t ordinal; + GeneratedStateKind generatedStateKind; + std::string_view stableName; constexpr auto operator<=>(const SpecificationKey &) const = default; }; @@ -60,20 +501,92 @@ export namespace mean_field::models { struct RuntimeSpecificationDescriptor final { SpecificationDescriptor specification; std::size_t canonicalIndex; - bool hasRootCompiler; + + // This reports only the self-owned physics declaration. Numerical + // support is queried from the operator compiler for the complete model. + bool hasDeclarativeDefinition; constexpr bool operator==(const RuntimeSpecificationDescriptor &) const = default; }; + namespace detail { + template struct IsModelTypeList : std::false_type {}; + + template struct IsModelTypeList> : std::true_type {}; + + template struct IsModelDefinition : std::false_type {}; + + template + struct IsModelDefinition< + ModelDefinition> + : std::bool_constant<(StableName.view().size() > 0) && + CompatibleSpecificationRoleAndGeneratedState && + IsModelTypeList::value && + IsModelTypeList::value>{}; + + template ::value> + struct DefinitionDescribesCandidate : std::false_type {}; + + template + struct DefinitionDescribesCandidate + : std::bool_constant> {}; + + template struct SpecificationDefinitionFor { + static constexpr bool available = false; + }; + + template + struct SpecificationDefinitionFor::ModelDefinition>> { + using Type = typename std::remove_cvref_t::ModelDefinition; + static constexpr bool available = DefinitionDescribesCandidate>::value; + }; + + // Compatibility projections for the two physical types that predate + // the self-describing front end. New types use only ModelDefinition. + template <> struct SpecificationDefinitionFor { + using Type = ConstitutiveLaw; + static constexpr bool available = true; + }; + + template <> struct SpecificationDefinitionFor { + using Type = BoundaryCondition; + static constexpr bool available = true; + }; + } // namespace detail + + template + concept SelfDescribingModelSpecification = requires { + typename std::remove_cvref_t::ModelDefinition; + } && detail::SpecificationDefinitionFor>::available; + + template + requires detail::SpecificationDefinitionFor>::available + using ModelDefinitionForT = typename detail::SpecificationDefinitionFor>::Type; + template struct SpecificationTraits; template - concept ModelSpecification = requires { - typename std::remove_cvref_t::Parameters; - { SpecificationTraits>::name } -> std::convertible_to; - { SpecificationTraits>::role } -> std::convertible_to; - { SpecificationTraits>::key } -> std::convertible_to; - } && std::constructible_from, typename std::remove_cvref_t::Parameters>; + requires detail::SpecificationDefinitionFor>::available + struct SpecificationTraits { + using Definition = ModelDefinitionForT; + + static constexpr std::string_view name = Definition::name; + static constexpr SpecificationRole role = Definition::role; + static constexpr SpecificationKey key{role, Definition::generatedStateKind, name}; + }; + + template + concept ModelSpecification = + detail::SpecificationDefinitionFor>::available && + requires { + typename std::remove_cvref_t::Parameters; + { SpecificationTraits>::name } -> std::convertible_to; + { SpecificationTraits>::role } -> std::convertible_to; + { SpecificationTraits>::key } -> std::convertible_to; + } && + std::constructible_from, typename std::remove_cvref_t::Parameters>; class FixedTotalMass final { public: @@ -81,19 +594,30 @@ export namespace mean_field::models { dimensions::MassValue Mtotal; }; - using TargetValue = dimensions::MassValue; + using ScalarDescription = DimensionalScalarConstraint< + dimensions::quantity::Mass, + dimensions::quantity::SpecificEnergy, + dimensions::quantity::Mass, + "fixed_total_mass.multiplier", + "C", + "fixed_total_mass.residual", + "R_M">; + using TargetValue = typename ScalarDescription::TargetValue; + using ModelDefinition = FixedIntegralWithMultiplier< + FixedTotalMass, "FixedTotalMass", + DependsOn, + Affects, + typename ScalarDescription::Normalization, + typename ScalarDescription::Manifest>; explicit FixedTotalMass(const Parameters parameters) : FixedTotalMass(parameters.Mtotal) { } explicit FixedTotalMass(const TargetValue targetMass) : m_targetMass(targetMass) { if (!std::isfinite(targetMass.value()) || targetMass.value() <= 0.0) { - throw std::invalid_argument( - std::format( - "The fixed total mass must be finite and positive. Instead M = {} was provided.", - targetMass.value() - ) - ); + throw std::invalid_argument(std::format("The fixed total mass must be finite and positive. " + "Instead M = {} was provided.", + targetMass.value())); } } @@ -101,29 +625,123 @@ export namespace mean_field::models { return m_targetMass; } + [[nodiscard]] TargetValue target() const noexcept { + return m_targetMass; + } + private: TargetValue m_targetMass; }; + class FixedAngularMomentum final { + public: + struct Parameters final { + dimensions::AngularMomentumValue Jtotal; + std::array axis{0.0, 0.0, 1.0}; + std::array center{0.0, 0.0, 0.0}; + }; + + using ScalarDescription = DimensionalScalarConstraint< + dimensions::quantity::AngularMomentum, + dimensions::quantity::AngularVelocity, + dimensions::quantity::AngularMomentum, + "fixed_angular_momentum.angular_velocity", + "Omega", + "fixed_angular_momentum.residual", + "R_J">; + using TargetValue = typename ScalarDescription::TargetValue; + using ModelDefinition = FixedIntegralWithPhysicalCoordinate< + FixedAngularMomentum, "FixedAngularMomentum", + DependsOn< + stellar::state::Density, + stellar::state::SurfaceShape, + stellar::state::OwnGeneratedCoordinate>, + Affects, + typename ScalarDescription::Normalization, + typename ScalarDescription::Manifest>; + + explicit FixedAngularMomentum(const Parameters parameters) + : m_targetAngularMomentum(parameters.Jtotal), m_axis(parameters.axis), m_center(parameters.center) { + if (!std::isfinite(m_targetAngularMomentum.value()) || m_targetAngularMomentum.value() < 0.0) { + throw std::invalid_argument(std::format("The fixed total angular momentum must be finite and " + "nonnegative. Instead J = {} was " + "provided.", + m_targetAngularMomentum.value())); + } + + double axisNormSquared = 0.0; + for (std::size_t component = 0; component < m_axis.size(); ++component) { + if (!std::isfinite(m_axis[component]) || !std::isfinite(m_center[component])) { + throw std::invalid_argument("A fixed-angular-momentum rotation axis and center must contain " + "only finite values."); + } + axisNormSquared += m_axis[component] * m_axis[component]; + } + if (!std::isfinite(axisNormSquared) || axisNormSquared <= 0.0) { + throw std::invalid_argument("A fixed-angular-momentum rotation axis must be nonzero."); + } + const double inverseAxisNorm = 1.0 / std::sqrt(axisNormSquared); + for (double &component : m_axis) { + component *= inverseAxisNorm; + } + } + + explicit FixedAngularMomentum(const TargetValue targetAngularMomentum) + : FixedAngularMomentum(Parameters{.Jtotal = targetAngularMomentum}) { + } + + [[nodiscard]] TargetValue targetAngularMomentum() const noexcept { + return m_targetAngularMomentum; + } + + [[nodiscard]] TargetValue target() const noexcept { + return m_targetAngularMomentum; + } + + [[nodiscard]] const std::array &axis() const noexcept { + return m_axis; + } + + [[nodiscard]] const std::array ¢er() const noexcept { + return m_center; + } + + private: + TargetValue m_targetAngularMomentum; + std::array m_axis; + std::array m_center; + }; + class FixedCentralDensity final { public: struct Parameters final { dimensions::DensityValue RhoC; }; - using TargetValue = dimensions::DensityValue; + using ScalarDescription = DimensionalScalarConstraint< + dimensions::quantity::Density, + dimensions::quantity::SpecificEnthalpy, + dimensions::quantity::SpecificEnthalpy, + "fixed_central_density.border", + "lambda_rho_c", + "fixed_central_density.residual", + "R_rho_c">; + using TargetValue = typename ScalarDescription::TargetValue; + using ModelDefinition = PhaseCondition< + FixedCentralDensity, "FixedCentralDensity", + DependsOn, + Affects, + typename ScalarDescription::Normalization, + typename ScalarDescription::Manifest>; explicit FixedCentralDensity(const Parameters parameters) : FixedCentralDensity(parameters.RhoC) { } explicit FixedCentralDensity(const TargetValue targetDensity) : m_targetDensity(targetDensity) { if (!std::isfinite(targetDensity.value()) || targetDensity.value() <= 0.0) { - throw std::invalid_argument( - std::format( - "The fixed central density must be finite and positive. Instead rho_c = {} was provided.", - targetDensity.value() - ) - ); + throw std::invalid_argument(std::format("The fixed central density must be finite and positive. " + "Instead rho_c = {} was provided.", + targetDensity.value())); } } @@ -131,104 +749,266 @@ export namespace mean_field::models { return m_targetDensity; } + [[nodiscard]] TargetValue target() const noexcept { + return m_targetDensity; + } + private: TargetValue m_targetDensity; }; - template <> struct SpecificationTraits { - static constexpr std::string_view name = "Polytrope"; - static constexpr SpecificationRole role = SpecificationRole::constitutive_law; - static constexpr SpecificationKey key{role, 0}; - }; - - template <> struct SpecificationTraits { - static constexpr std::string_view name = "IsobaricSurface"; - static constexpr SpecificationRole role = SpecificationRole::boundary_condition; - static constexpr SpecificationKey key{role, 0}; - }; - - template <> struct SpecificationTraits { - static constexpr std::string_view name = "FixedTotalMass"; - static constexpr SpecificationRole role = SpecificationRole::invariant; - static constexpr SpecificationKey key{role, 0}; - }; - - template <> struct SpecificationTraits { - static constexpr std::string_view name = "FixedCentralDensity"; - static constexpr SpecificationRole role = SpecificationRole::phase_condition; - static constexpr SpecificationKey key{role, 0}; - }; - - template struct ModelTypeList final { - static constexpr std::size_t size = sizeof...(Types); - }; - template struct ModelTypeListContains; template struct ModelTypeListContains> - : std::bool_constant<(std::same_as || ...)> { }; + : std::bool_constant<(std::same_as || ...)> {}; template inline constexpr bool modelTypeListContains = ModelTypeListContains::value; - template struct ResidualFor final { - using SpecificationType = Specification; + template struct ResidualFor final { + using SpecificationType = Specification; static constexpr std::size_t scalarArity = 1; }; - template struct MultiplierFor final { - using SpecificationType = Specification; + template struct MultiplierFor final { + using SpecificationType = Specification; static constexpr std::size_t scalarArity = 1; }; - template struct BorderFor final { - using SpecificationType = Specification; + // A generated state variable that participates directly in the physical + // equations, rather than serving only as a Lagrange multiplier or border. + template struct PhysicalCoordinateFor final { + using SpecificationType = Specification; static constexpr std::size_t scalarArity = 1; }; + template struct BorderFor final { + using SpecificationType = Specification; + + static constexpr std::size_t scalarArity = 1; + }; + + template + concept CoordinateNormalizationDefinition = requires { + { Candidate::topology } -> std::convertible_to; + { Candidate::scale } -> std::convertible_to; + { Candidate::available } -> std::convertible_to; + }; + + template + concept GeneratedNormalizationDefinition = requires { + typename Candidate::Value; + typename Candidate::Residual; + requires CoordinateNormalizationDefinition; + requires CoordinateNormalizationDefinition; + { Candidate::available } -> std::convertible_to; + }; + + template + concept GeneratedManifestDefinition = requires { + { Candidate::valueStableId } -> std::convertible_to; + { Candidate::valueSymbol } -> std::convertible_to; + { Candidate::residualStableId } -> std::convertible_to; + { Candidate::residualSymbol } -> std::convertible_to; + { Candidate::targetUnits } -> std::convertible_to; + { Candidate::residualUnits } -> std::convertible_to; + { Candidate::available } -> std::convertible_to; + }; + + namespace detail { + template struct GeneratedSignatureFor; + + template struct GeneratedSignatureFor { + using Values = ModelTypeList<>; + using Residuals = ModelTypeList<>; + }; + + template struct GeneratedSignatureFor { + using Values = ModelTypeList>; + using Residuals = ModelTypeList>; + }; + + template + struct GeneratedSignatureFor { + using Values = ModelTypeList>; + using Residuals = ModelTypeList>; + }; + + template + struct GeneratedSignatureFor { + using Values = ModelTypeList>; + using Residuals = ModelTypeList>; + }; + + template struct SafeGeneratedNormalization { + using Type = UnavailableGeneratedNormalization; + }; + + template struct SafeGeneratedNormalization { + using Type = Candidate; + }; + + template struct SafeGeneratedManifest { + using Type = UnavailableGeneratedManifest; + }; + + template struct SafeGeneratedManifest { + using Type = Candidate; + }; + } // namespace detail + template struct SpecificationContribution { - using GeneratedValues = ModelTypeList<>; - using GeneratedResiduals = ModelTypeList<>; + private: + using CanonicalSpecification = std::remove_cvref_t; + using Definition = ModelDefinitionForT; + using Signature = detail::GeneratedSignatureFor; - static constexpr bool isDefined = false; - static constexpr bool hasRootCompiler = false; + public: + using SpecificationType = CanonicalSpecification; + using ModelDefinition = Definition; + using GeneratedValues = typename Signature::Values; + using GeneratedResiduals = typename Signature::Residuals; + using DependsOn = typename Definition::DependsOn; + using Affects = typename Definition::Affects; + using Normalization = typename detail::SafeGeneratedNormalization::Type; + using Manifest = typename detail::SafeGeneratedManifest::Type; + + static constexpr GeneratedStateKind generatedStateKind = Definition::generatedStateKind; + static constexpr std::size_t generatedValueArity = Definition::generatedValueArity; + static constexpr std::size_t generatedResidualArity = Definition::generatedResidualArity; + static constexpr bool isDefined = Definition::structurallyAvailable; + static constexpr bool hasDeclarativeDefinition = Definition::structurallyAvailable; }; - template <> struct SpecificationContribution { - using GeneratedValues = ModelTypeList<>; - using GeneratedResiduals = ModelTypeList<>; + namespace detail { + template + [[nodiscard]] consteval bool generatedScalarDimensionsAreCoherent() { + using Contribution = SpecificationContribution; + using Normalization = typename Contribution::Normalization; + using Manifest = typename Contribution::Manifest; - static constexpr bool isDefined = true; - static constexpr bool hasRootCompiler = true; - }; + if constexpr (Contribution::generatedValueArity == 0) { + return true; + } else { + constexpr bool normalizationIsTyped = requires { + typename Normalization::TargetQuantity; + typename Normalization::GeneratedCoordinateQuantity; + typename Normalization::ConstraintResidualQuantity; + typename Normalization::TargetValue; + { + Normalization::targetScale + } -> std::convertible_to; + }; + constexpr bool manifestIsTyped = requires { + typename Manifest::TargetQuantity; + typename Manifest::GeneratedCoordinateQuantity; + typename Manifest::ConstraintResidualQuantity; + }; - template <> struct SpecificationContribution { - using GeneratedValues = ModelTypeList<>; - using GeneratedResiduals = ModelTypeList<>; + /* The lower-level declaration API remains a deliberate + * compatibility escape hatch. Once either half opts into the + * dimensional protocol, however, the complete typed contract + * is mandatory and cannot be mixed with free-form metadata. */ + if constexpr (!normalizationIsTyped && !manifestIsTyped) { + return true; + } else if constexpr (!normalizationIsTyped || !manifestIsTyped) { + return false; + } else { + using TargetQuantity = typename Normalization::TargetQuantity; + using GeneratedCoordinateQuantity = + typename Normalization::GeneratedCoordinateQuantity; + using ConstraintResidualQuantity = + typename Normalization::ConstraintResidualQuantity; + using ValueNormalization = typename Normalization::Value; + using ResidualNormalization = typename Normalization::Residual; - static constexpr bool isDefined = true; - static constexpr bool hasRootCompiler = true; - }; + if constexpr ( + !PhysicalScaleRepresentedQuantity || + !PhysicalScaleRepresentedQuantity || + !PhysicalScaleRepresentedQuantity) { + return false; + } else if constexpr (!requires { + typename std::integral_constant< + PhysicalScaleLaw, + static_cast( + Normalization::targetScale)>; + typename std::integral_constant< + PhysicalScaleLaw, + static_cast( + ValueNormalization::scale)>; + typename std::integral_constant< + PhysicalScaleLaw, + static_cast( + ResidualNormalization::scale)>; + typename std::bool_constant< + static_cast( + Manifest::targetUnits) == + TargetQuantity::identifier>; + typename std::bool_constant< + static_cast( + Manifest::residualUnits) == + ConstraintResidualQuantity::identifier>; + }) { + return false; + } else if constexpr (!requires(const Specification &specification) { + specification.target(); + }) { + return false; + } else { + return + std::same_as< + typename Normalization::TargetValue, + dimensions::QuantityValue> && + Normalization::targetScale == + physicalScaleForQuantity && + std::same_as && + std::same_as< + GeneratedCoordinateQuantity, + typename Manifest::GeneratedCoordinateQuantity> && + std::same_as< + ConstraintResidualQuantity, + typename Manifest::ConstraintResidualQuantity> && + ValueNormalization::scale == + physicalScaleForQuantity && + ResidualNormalization::scale == + physicalScaleForQuantity && + static_cast(Manifest::targetUnits) == + TargetQuantity::identifier && + static_cast(Manifest::residualUnits) == + ConstraintResidualQuantity::identifier && + std::same_as< + std::remove_cvref_t().target())>, + typename Normalization::TargetValue>; + } + } + } + } + } // namespace detail - template <> struct SpecificationContribution { - using GeneratedValues = ModelTypeList>; - using GeneratedResiduals = ModelTypeList>; + template + concept CompleteGeneratedScalarDimensionsFor = + ModelSpecification && + detail::generatedScalarDimensionsAreCoherent< + std::remove_cvref_t>(); - static constexpr bool isDefined = true; - static constexpr bool hasRootCompiler = true; - }; + template + concept CompleteGeneratedNormalizationFor = + ModelSpecification && + CompleteGeneratedScalarDimensionsFor && + (SpecificationContribution>::generatedValueArity == 0 || + SpecificationContribution>::Normalization::available); - template <> struct SpecificationContribution { - using GeneratedValues = ModelTypeList>; - using GeneratedResiduals = ModelTypeList>; - - static constexpr bool isDefined = true; - static constexpr bool hasRootCompiler = true; - }; + template + concept CompleteGeneratedManifestFor = + ModelSpecification && + CompleteGeneratedScalarDimensionsFor && + (SpecificationContribution>::generatedValueArity == 0 || + SpecificationContribution>::Manifest::available); template concept ResolvedModelSpecification = @@ -254,6 +1034,29 @@ export namespace mean_field::models { using Type = typename ConcatenateModelTypeLists, Remaining...>::Type; }; + template struct SpecificationsForRole { + using Type = ModelTypeList<>; + + static constexpr bool available = false; + static constexpr std::size_t count = 0; + }; + + template + struct SpecificationsForRole> { + using Type = typename ConcatenateModelTypeLists< + std::conditional_t::role == Role, ModelTypeList, + ModelTypeList<>>...>::Type; + + static constexpr bool available = true; + static constexpr std::size_t count = Type::size; + }; + + template struct UniqueModelType; + + template struct UniqueModelType> { + using TypeValue = Type; + }; + template struct InsertSpecification; template @@ -274,10 +1077,9 @@ export namespace mean_field::models { }; public: - using Type = std::conditional_t< - (SpecificationTraits::key < SpecificationTraits::key), - SpecificationSetStorage, - typename PrependSpecification::Type>; + using Type = std::conditional_t<(SpecificationTraits::key < SpecificationTraits::key), + SpecificationSetStorage, + typename PrependSpecification::Type>; }; template struct CanonicalizeSpecifications; @@ -296,21 +1098,21 @@ export namespace mean_field::models { using CanonicalSpecificationSet = typename CanonicalizeSpecifications, Specifications...>::Type; - template < - ModelSpecification Head, - ModelSpecification... Tail> - consteval bool specificationKeyIsUnique() { - return ((SpecificationTraits::key != SpecificationTraits::key) && ...); + template consteval bool specificationKeyIsUnique() { + constexpr auto headKey = SpecificationTraits::key; + return ((headKey.role != SpecificationTraits::key.role || + headKey.stableName != SpecificationTraits::key.stableName) && + ...); } template struct SpecificationKeysAreUnique; - template <> struct SpecificationKeysAreUnique<> : std::true_type { }; + template <> struct SpecificationKeysAreUnique<> : std::true_type {}; template struct SpecificationKeysAreUnique - : std::bool_constant< - specificationKeyIsUnique() && SpecificationKeysAreUnique::value> { }; + : std::bool_constant() && + SpecificationKeysAreUnique::value> {}; template inline constexpr std::size_t specificationRoleCount = @@ -320,7 +1122,7 @@ export namespace mean_field::models { template struct ModelTypeListScalarArity> - : std::integral_constant { }; + : std::integral_constant {}; template inline constexpr bool isOneOf = (std::same_as || ...); @@ -334,23 +1136,50 @@ export namespace mean_field::models { template struct ArgumentsMatchCanonicalSpecifications, Arguments...> - : std::bool_constant< - sizeof...(CanonicalSpecifications) == sizeof...(Arguments) && - (isOneOf, CanonicalSpecifications...> && ...) && - ((typeCount == 1) && ...)> { }; + : std::bool_constant, CanonicalSpecifications...> && ...) && + ((typeCount == 1) && ...)> {}; } // namespace detail - template - inline constexpr bool specificationKeysAreUnique = detail::SpecificationKeysAreUnique::value; + template + requires(ModelSpecification> && ...) + inline constexpr bool specificationKeysAreUnique = + detail::SpecificationKeysAreUnique...>::value; template concept ValidModelSpecificationPack = - (ResolvedModelSpecification && ...) && specificationKeysAreUnique && - detail::specificationRoleCount == 1; + (ResolvedModelSpecification> && ...) && + specificationKeysAreUnique...> && + detail::specificationRoleCount...> == 1; - template - requires specificationKeysAreUnique - using SpecificationSet = detail::CanonicalSpecificationSet; + template + requires(ModelSpecification> && ...) && + specificationKeysAreUnique...> + using SpecificationSet = detail::CanonicalSpecificationSet...>; + + template + using SpecificationsForRoleT = + typename detail::SpecificationsForRole>::Type; + + template + inline constexpr std::size_t specificationRoleCount = + detail::SpecificationsForRole>::count; + + template + concept HasSpecificationsForRole = + detail::SpecificationsForRole>::available && + specificationRoleCount > 0; + + template + concept HasUniqueSpecificationForRole = + detail::SpecificationsForRole>::available && + specificationRoleCount == 1; + + template + requires HasUniqueSpecificationForRole + using SpecificationForRoleT = + typename detail::UniqueModelType>::TypeValue; template struct SpecificationOperatorSignature; @@ -374,13 +1203,12 @@ export namespace mean_field::models { [[nodiscard]] consteval SpecificationDescriptor specificationDescriptor() { using Contribution = SpecificationContribution; - return { - .name = SpecificationTraits::name, - .role = SpecificationTraits::role, - .key = SpecificationTraits::key, - .generatedValueArity = detail::ModelTypeListScalarArity::value, - .generatedResidualArity = detail::ModelTypeListScalarArity::value - }; + return {.name = SpecificationTraits::name, + .role = SpecificationTraits::role, + .key = SpecificationTraits::key, + .generatedValueArity = detail::ModelTypeListScalarArity::value, + .generatedResidualArity = + detail::ModelTypeListScalarArity::value}; } namespace detail { @@ -388,34 +1216,55 @@ export namespace mean_field::models { template class SpecifiedModel> final { + private: + struct CanonicalArgumentsTag final { }; + + /* + * Capture the user-spelled pack once, then move each exact type + * into its canonical slot. Reconstructing an argument tuple in + * the pack expansion would forward every argument once per + * specification and silently consume move-sensitive physics + * objects multiple times. + */ + template + explicit SpecifiedModel( + std::tuple &&arguments, + CanonicalArgumentsTag + ) + : m_specifications( + std::get(std::move(arguments))... + ) { + } + public: - using SpecificationTypes = SpecificationSetStorage; - using OperatorSignature = SpecificationOperatorSignature; + using SpecificationTypes = SpecificationSetStorage; + using OperatorSignature = SpecificationOperatorSignature; static constexpr bool symbolicallySquare = OperatorSignature::symbolicallySquare; - static constexpr bool hasCompleteRootCompiler = - (SpecificationContribution::hasRootCompiler && ...); - static constexpr EquilibriumSystemCompilation compilationClass = - symbolicallySquare && hasCompleteRootCompiler - ? EquilibriumSystemCompilation::complete_equilibrium_system - : EquilibriumSystemCompilation::equation_contributions_only; + + // A declaration can be complete before every physics name has a + // backend block mapping. Keep this deliberately separate from + // operators::StellarEquilibriumSystemCompilable. + static constexpr bool hasCompleteEquilibriumDeclaration = + symbolicallySquare && (SpecificationContribution::hasDeclarativeDefinition && ...); template - requires ArgumentsMatchCanonicalSpecifications< - SpecificationTypes, - Arguments...>::value + requires ArgumentsMatchCanonicalSpecifications::value && + std::constructible_from< + std::tuple...>, + Arguments...> && + (std::constructible_from && ...) explicit SpecifiedModel(Arguments &&...arguments) - : m_specifications( - std::get( - std::tuple...>{std::forward(arguments)...} - )... + : SpecifiedModel( + std::tuple...>{ + std::forward(arguments)... + }, + CanonicalArgumentsTag{} ) { } template - requires isOneOf< - Specification, - Specifications...> + requires isOneOf [[nodiscard]] const Specification &specification() const noexcept { return std::get(m_specifications); } @@ -433,10 +1282,10 @@ export namespace mean_field::models { runtimeDescriptors = [] { std::array descriptors{}; std::size_t index = 0; - ((descriptors[index] = - {.specification = specificationDescriptor(), - .canonicalIndex = index, - .hasRootCompiler = SpecificationContribution::hasRootCompiler}, + ((descriptors[index] = {.specification = specificationDescriptor(), + .canonicalIndex = index, + .hasDeclarativeDefinition = + SpecificationContribution::hasDeclarativeDefinition}, ++index), ...); return descriptors; @@ -456,7 +1305,7 @@ export namespace mean_field::models { typename std::remove_cvref_t::SpecificationTypes; typename std::remove_cvref_t::OperatorSignature; requires std::remove_cvref_t::symbolicallySquare; - { std::remove_cvref_t::compilationClass } -> std::convertible_to; + { std::remove_cvref_t::hasCompleteEquilibriumDeclaration } -> std::convertible_to; { std::remove_cvref_t::runtimeSpecificationDescriptors() } -> std::same_as>; @@ -465,17 +1314,186 @@ export namespace mean_field::models { static_assert(ModelSpecification); static_assert(ModelSpecification); static_assert(ModelSpecification); + static_assert(ModelSpecification); static_assert(ModelSpecification); static_assert(ResolvedModelSpecification); static_assert(ResolvedModelSpecification); static_assert(ResolvedModelSpecification); + static_assert(ResolvedModelSpecification); static_assert(ResolvedModelSpecification); + static_assert(CompleteGeneratedScalarDimensionsFor); + static_assert(CompleteGeneratedScalarDimensionsFor); + static_assert(CompleteGeneratedScalarDimensionsFor); } // namespace mean_field::models +/* + * Astronomer-facing declaration vocabulary. These aliases deliberately + * package the backend model lists, normalization topology, and manifest pair + * without duplicating any compiler logic. Advanced code may still use the + * models:: spellings directly; both paths produce exactly the same types. + */ +export namespace mean_field::stellar { + namespace state { + using Density = models::stellar::state::Density; + using SurfaceShape = models::stellar::state::SurfaceShape; + using GravityGradient = models::stellar::state::GravityGradient; + using GravitationalPotential = models::stellar::state::GravitationalPotential; + using SpecificEnthalpy = models::stellar::state::SpecificEnthalpy; + using OwnGeneratedCoordinate = models::stellar::state::OwnGeneratedCoordinate; + + template + using GeneratedCoordinateOf = models::stellar::state::GeneratedCoordinateOf; + } // namespace state + + namespace equation { + using GravityGradientDefinition = models::stellar::equation::GravityGradientDefinition; + using PoissonEquation = models::stellar::equation::PoissonEquation; + using DensityClosure = models::stellar::equation::DensityClosure; + using SurfaceShapeBalance = models::stellar::equation::SurfaceShapeBalance; + using HydrostaticBalance = models::stellar::equation::HydrostaticBalance; + using OwnConstraint = models::stellar::equation::OwnConstraint; + + template + using ConstraintOf = models::stellar::equation::ConstraintOf; + } // namespace equation + + template + using Derivative = models::stellar::Derivative; + + template + using Reads = models::DependsOn; + + template + using Changes = models::Affects; + + using PhysicalScale = models::PhysicalScaleLaw; + + template < + models::PhysicalScaleRepresentedQuantity TargetQuantity, + models::PhysicalScaleRepresentedQuantity GeneratedCoordinateQuantity, + models::PhysicalScaleRepresentedQuantity ConstraintResidualQuantity, + models::FixedString ValueStableId, + models::FixedString ValueSymbol, + models::FixedString ResidualStableId, + models::FixedString ResidualSymbol> + using ScalarConstraint = models::DimensionalScalarConstraint< + TargetQuantity, + GeneratedCoordinateQuantity, + ConstraintResidualQuantity, + ValueStableId, + ValueSymbol, + ResidualStableId, + ResidualSymbol>; + + template + concept ScalarConstraintDescription = requires { + typename std::remove_cvref_t::Normalization; + typename std::remove_cvref_t::Manifest; + typename std::remove_cvref_t::TargetQuantity; + typename std::remove_cvref_t::GeneratedCoordinateQuantity; + typename std::remove_cvref_t::ConstraintResidualQuantity; + typename std::remove_cvref_t::TargetValue; + requires models::GeneratedNormalizationDefinition< + typename std::remove_cvref_t::Normalization>; + requires models::GeneratedManifestDefinition< + typename std::remove_cvref_t::Manifest>; + requires std::remove_cvref_t::Normalization::available; + requires std::remove_cvref_t::Manifest::available; + requires std::remove_cvref_t::dimensionallyTyped; + }; +} // namespace mean_field::stellar + export namespace mean_field::integral { - using FixedTotalMass = models::FixedTotalMass; -} + using FixedTotalMass = models::FixedTotalMass; + using FixedAngularMomentum = models::FixedAngularMomentum; + + template , + typename Affects = models::ModelTypeList<>, + typename Normalization = models::UnavailableGeneratedNormalization, + typename Manifest = models::UnavailableGeneratedManifest> + using FixedIntegralWithMultiplier = + models::FixedIntegralWithMultiplier; + + template , + typename Affects = models::ModelTypeList<>, + typename Normalization = models::UnavailableGeneratedNormalization, + typename Manifest = models::UnavailableGeneratedManifest> + using FixedWithMultiplier = + FixedIntegralWithMultiplier; + + template , + typename Affects = models::ModelTypeList<>, + typename Normalization = models::UnavailableGeneratedNormalization, + typename Manifest = models::UnavailableGeneratedManifest> + using FixedIntegralWithPhysicalCoordinate = + models::FixedIntegralWithPhysicalCoordinate; + + template , + typename Affects = models::ModelTypeList<>, + typename Normalization = models::UnavailableGeneratedNormalization, + typename Manifest = models::UnavailableGeneratedManifest> + using FixedWithPhysicalCoordinate = + FixedIntegralWithPhysicalCoordinate; + + template < + typename Specification, + models::FixedString Name, + typename Reads, + typename Changes, + stellar::ScalarConstraintDescription Description> + using FixedScalarWithMultiplier = models::FixedIntegralWithMultiplier< + Specification, + Name, + Reads, + Changes, + typename Description::Normalization, + typename Description::Manifest>; + + template < + typename Specification, + models::FixedString Name, + typename Reads, + typename Changes, + stellar::ScalarConstraintDescription Description> + using FixedScalarWithPhysicalCoordinate = models::FixedIntegralWithPhysicalCoordinate< + Specification, + Name, + Reads, + Changes, + typename Description::Normalization, + typename Description::Manifest>; +} // namespace mean_field::integral export namespace mean_field::constraint { using FixedCentralDensity = models::FixedCentralDensity; + + template , + typename Affects = models::ModelTypeList<>, + typename Normalization = models::UnavailableGeneratedNormalization, + typename Manifest = models::UnavailableGeneratedManifest> + using PhaseCondition = models::PhaseCondition; + + template < + typename Specification, + models::FixedString Name, + typename Reads, + typename Changes, + stellar::ScalarConstraintDescription Description> + using ScalarPhaseCondition = models::PhaseCondition< + Specification, + Name, + Reads, + Changes, + typename Description::Normalization, + typename Description::Manifest>; +} // namespace mean_field::constraint + +export namespace mean_field::eos { + template + using ConstitutiveLaw = models::ConstitutiveLaw; +} + +export namespace mean_field::surface { + template + using BoundaryCondition = models::BoundaryCondition; } diff --git a/libmeanfield/interface/models/typed_stellar_model.cppm b/libmeanfield/interface/models/typed_stellar_model.cppm index a7cfc1f..228f81f 100644 --- a/libmeanfield/interface/models/typed_stellar_model.cppm +++ b/libmeanfield/interface/models/typed_stellar_model.cppm @@ -14,21 +14,41 @@ export namespace mean_field::model { template class StellarModel; template + requires models::ValidModelSpecificationPack && + models::SpecificationOperatorSignature< + models::detail::SpecificationSetStorage>::symbolicallySquare class StellarModel> final { public: using SpecificationTypes = models::detail::SpecificationSetStorage; using OperatorSignature = models::SpecificationOperatorSignature; using Storage = models::Model; + using EquationOfStateType = + models::SpecificationForRoleT; - static constexpr std::size_t specificationCount = sizeof...(CanonicalSpecifications); - static constexpr bool symbolicallySquare = Storage::symbolicallySquare; - static constexpr bool hasCompleteEquilibriumCompiler = Storage::hasCompleteRootCompiler; - static constexpr models::EquilibriumSystemCompilation compilationClass = Storage::compilationClass; + static constexpr std::size_t specificationCount = sizeof...(CanonicalSpecifications); + static constexpr bool symbolicallySquare = Storage::symbolicallySquare; + static constexpr bool hasCompleteEquilibriumDeclaration = + Storage::hasCompleteEquilibriumDeclaration; + + template + using SpecificationsForRole = models::SpecificationsForRoleT; + + template + requires models::HasUniqueSpecificationForRole + using SpecificationForRole = models::SpecificationForRoleT; + + template + static constexpr std::size_t specificationRoleCount = models::specificationRoleCount; + + template + static constexpr bool hasSpecificationsForRole = models::HasSpecificationsForRole; + + template + static constexpr bool hasUniqueSpecificationForRole = + models::HasUniqueSpecificationForRole; template - requires std::constructible_from< - Storage, - Arguments...> + requires std::constructible_from explicit StellarModel(Arguments &&...arguments) : m_specifications(std::forward(arguments)...) { } @@ -41,6 +61,25 @@ export namespace mean_field::model { template static constexpr bool containsSpecification = Storage::template containsSpecification; + template + requires models::HasUniqueSpecificationForRole + [[nodiscard]] const models::SpecificationForRoleT & + specificationForRole() const noexcept { + using Specification = models::SpecificationForRoleT; + return specification(); + } + + [[nodiscard]] const EquationOfStateType &equationOfState() const noexcept { + return specificationForRole(); + } + + template + requires models::HasUniqueSpecificationForRole + [[nodiscard]] const auto &surfaceCondition() const noexcept { + return specificationForRole(); + } + [[nodiscard]] static constexpr std::span runtimeSpecificationDescriptors() noexcept { return Storage::runtimeSpecificationDescriptors(); @@ -56,11 +95,69 @@ export namespace mean_field::model { -> StellarModel...>>; namespace detail { - template struct IsStellarModel : std::false_type { }; + template struct IsStellarModel : std::false_type {}; - template struct IsStellarModel> : std::true_type { }; + template + struct IsStellarModel< + StellarModel, + std::void_t::SpecificationTypes, + typename StellarModel::OperatorSignature, + decltype(StellarModel::specificationCount), + decltype(StellarModel::hasCompleteEquilibriumDeclaration)>> + : std::true_type {}; + + template ::value> + struct StellarModelRoleSelection { + using Types = models::ModelTypeList<>; + + static constexpr std::size_t count = 0; + }; + + template + struct StellarModelRoleSelection { + using Types = models::SpecificationsForRoleT; + + static constexpr std::size_t count = + models::specificationRoleCount; + }; } // namespace detail template concept StellarModelType = detail::IsStellarModel>::value; + + template + inline constexpr std::size_t specificationRoleCount = + detail::StellarModelRoleSelection>::count; + + template + concept HasSpecificationsForRole = StellarModelType && specificationRoleCount > 0; + + template + concept HasUniqueSpecificationForRole = StellarModelType && specificationRoleCount == 1; + + template + requires StellarModelType + using SpecificationsForRoleT = + typename detail::StellarModelRoleSelection>::Types; + + template + requires HasUniqueSpecificationForRole + using SpecificationForRoleT = + models::SpecificationForRoleT::SpecificationTypes>; + + template + concept HasEquationOfState = HasUniqueSpecificationForRole; + + template + concept HasSurfaceCondition = HasSpecificationsForRole; + + template + concept HasUniqueSurfaceCondition = + HasUniqueSpecificationForRole; + + template + using EquationOfStateType = SpecificationForRoleT; + + template + using SurfaceConditionType = SpecificationForRoleT; } // namespace mean_field::model diff --git a/libmeanfield/interface/normalization/normalization.cppm b/libmeanfield/interface/normalization/normalization.cppm new file mode 100644 index 0000000..d478d1a --- /dev/null +++ b/libmeanfield/interface/normalization/normalization.cppm @@ -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; diff --git a/libmeanfield/interface/normalization/operators.cppm b/libmeanfield/interface/normalization/operators.cppm new file mode 100644 index 0000000..fe68892 --- /dev/null +++ b/libmeanfield/interface/normalization/operators.cppm @@ -0,0 +1,621 @@ +module; + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +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 + concept RuntimePreparedNormalizationOperation = + requires(const std::remove_cvref_t &problem) { + typename std::remove_cvref_t::NormalizationPrescriptionType; + typename std::remove_cvref_t::FormType; + requires RuntimePreparedNormalizationFor< + typename std::remove_cvref_t::NormalizationPrescriptionType, + typename std::remove_cvref_t::FormType>; + { + problem.GetNormalizationPrescription() + } -> std::same_as::NormalizationPrescriptionType &>; + { + prepareStellarNormalization( + problem.GetNormalizationPrescription(), + problem) + } -> std::same_as; + }; + + template + requires utils::blocks::block_form_is_valid_v
+ class DiagonalNormalizationBuilder final { + public: + explicit DiagonalNormalizationBuilder(const utils::blocks::form_layout &layout) + : m_layout(&layout), + m_stateFactors(layout.value_offsets().Last()), + m_residualFactors(layout.residual_offsets().Last()) { + } + + explicit DiagonalNormalizationBuilder( + utils::blocks::form_layout && + ) = delete; + + explicit DiagonalNormalizationBuilder( + const utils::blocks::form_layout && + ) = delete; + + template + requires utils::blocks::contains_type_v + void SetValueBlock( + const double physicalScale, + const mfem::Vector &primalGramDiagonal + ) { + constexpr int block = utils::blocks::type_index_v; + 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 + requires utils::blocks::contains_type_v + void SetResidualBlock( + const double physicalScale, + const mfem::Vector &primalGramDiagonal + ) { + constexpr int block = utils::blocks::type_index_v; + 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 + requires utils::blocks::contains_type_v + void SetValueGlobal(const double physicalScale) { + constexpr int block = utils::blocks::type_index_v; + SetConstantMetricValueBlock(physicalScale, BlockSize(m_layout->value_offsets(), block)); + } + + template + requires utils::blocks::contains_type_v + void SetResidualGlobal(const double physicalScale) { + constexpr int block = utils::blocks::type_index_v; + mfem::Vector metric(BlockSize(m_layout->residual_offsets(), block)); + metric = 1.0; + SetResidualBlock(physicalScale, metric); + } + + template + requires utils::blocks::contains_type_v + void SetHybridResidualBlock( + const double physicalScale, + const mfem::Vector &bulkPrimalGramDiagonal, + const std::span pointRows, + const double pointMetric = 1.0 + ) { + constexpr int block = utils::blocks::type_index_v; + 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 isPointRow(static_cast(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(row)]) { + throw std::invalid_argument("A hybrid point row was supplied more than once."); + } + isPointRow[static_cast(row)] = true; + } + + mfem::Vector metric(size); + for (int row = 0; row < size; ++row) { + metric(row) = isPointRow[static_cast(row)] + ? pointMetric + : bulkPrimalGramDiagonal(row); + } + SetResidualBlock(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 + void SetConstantMetricValueBlock( + const double physicalScale, + const int size + ) { + mfem::Vector metric(size); + metric = 1.0; + SetValueBlock(physicalScale, metric); + } + + [[nodiscard]] static int BlockSize( + const mfem::Array &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 *m_layout; + mfem::Vector m_stateFactors; + mfem::Vector m_residualFactors; + std::array m_valueAssigned{}; + std::array 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 diff --git a/libmeanfield/interface/normalization/physical_riesz.cppm b/libmeanfield/interface/normalization/physical_riesz.cppm new file mode 100644 index 0000000..c8635e6 --- /dev/null +++ b/libmeanfield/interface/normalization/physical_riesz.cppm @@ -0,0 +1,728 @@ +module; + +#include +#include +#include +#include + +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 + concept RieszGeometryPolicy = std::same_as, ReferenceGeometry>; + + template + concept ReferenceScalePolicy = std::same_as, 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; + + template struct IsPhysicalRieszDiagonal : std::false_type { }; + + template + struct IsPhysicalRieszDiagonal> : std::true_type { }; + + template + concept PhysicalRieszDiagonalPrescription = + IsPhysicalRieszDiagonal>::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 + requires requires(const Model &model) { + { + model.template specification() + } -> std::same_as; + { + model.template specification().targetMass() + } -> std::same_as; + } + [[nodiscard]] StellarCharacteristicScales deriveStellarCharacteristicScales( + const PhysicalRieszDiagonal &prescription, + const Model &model + ) { + return deriveStellarCharacteristicScales( + model.template specification().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 struct DeclaredRieszTopology { + static constexpr bool available = false; + static constexpr RieszTopology value = RieszTopology::identity; + }; + +#define MEAN_FIELD_DECLARED_RIESZ_TOPOLOGY(Name) \ + template <> struct DeclaredRieszTopology { \ + 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 struct DeclaredPhysicalScale { + static constexpr bool available = false; + static constexpr PhysicalScaleKind value = PhysicalScaleKind::dimensionless; + }; + +#define MEAN_FIELD_DECLARED_PHYSICAL_SCALE(Name) \ + template <> struct DeclaredPhysicalScale { \ + 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 + struct CompileDeclaredPhysicalRieszCoordinate { + using Method = UnsupportedPhysicalRieszCoordinate; + static constexpr bool registered = false; + }; + + template + struct CompileDeclaredPhysicalRieszCoordinate< + Declaration, + std::void_t< + decltype(std::integral_constant< + models::RieszTopology, + static_cast(Declaration::topology)>{}), + decltype(std::integral_constant< + models::PhysicalScaleLaw, + static_cast(Declaration::scale)>{}), + decltype(std::bool_constant(Declaration::available)>{})>> { + private: + static constexpr models::RieszTopology declaredTopology = + static_cast(Declaration::topology); + static constexpr models::PhysicalScaleLaw declaredScale = + static_cast(Declaration::scale); + using Topology = DeclaredRieszTopology; + using Scale = DeclaredPhysicalScale; + + public: + static constexpr bool registered = static_cast(Declaration::available) && + Topology::available && Scale::available; + using Method = std::conditional_t< + registered, + PhysicalRieszCoordinate, + UnsupportedPhysicalRieszCoordinate>; + }; + + template + struct DeclaredGeneratedPhysicalRieszCoordinate { + using Method = UnsupportedPhysicalRieszCoordinate; + static constexpr bool registered = false; + }; + + template + 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 + 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 + struct GeneratedPhysicalRieszCoverage { + static constexpr bool complete = false; + }; + + template + struct GeneratedPhysicalRieszCoverage< + models::ModelTypeList, + models::ModelTypeList> { + static constexpr bool complete = + (DeclaredGeneratedPhysicalRieszCoordinate< + GeneratedValues, + CoordinateKind::value>::registered && ...) && + (DeclaredGeneratedPhysicalRieszCoordinate< + GeneratedResiduals, + CoordinateKind::residual>::registered && ...); + }; + + template + struct SpecificationPhysicalRieszCoverage { + static constexpr bool complete = false; + }; + + template + struct SpecificationPhysicalRieszCoverage< + Specification, + std::void_t< + typename models::SpecificationContribution::GeneratedValues, + typename models::SpecificationContribution::GeneratedResiduals>> + : GeneratedPhysicalRieszCoverage< + typename models::SpecificationContribution::GeneratedValues, + typename models::SpecificationContribution::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 + struct PhysicalRieszBlockTraits> + : detail::DeclaredGeneratedPhysicalRieszCoordinate { }; + + template + struct PhysicalRieszBlockTraits> + : detail::DeclaredGeneratedPhysicalRieszCoordinate { }; + + template + concept GeneratedValuePhysicalRieszNormalizable = + detail::DeclaredGeneratedPhysicalRieszCoordinate::registered; + + template + concept GeneratedResidualPhysicalRieszNormalizable = + detail::DeclaredGeneratedPhysicalRieszCoordinate::registered; + + template + concept CompleteGeneratedPhysicalRieszNormalizationFor = + detail::SpecificationPhysicalRieszCoverage>::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 + concept PhysicalRieszCoreRuntime = + requires(const std::remove_cvref_t &core) { + { + core.GetGravityContext().GetDensityMap() + } -> std::same_as; + { + core.GetGravityContext().GetGravityGradientMap() + } -> std::same_as; + { + core.GetGravityContext().GetGravityPotentialMap() + } -> std::same_as; + { + core.GetHydrostaticOperator().GetEnthalpyMap() + } -> std::same_as; + { + core.GetDomainDeformation().parameterCount() + } -> std::same_as; + }; + + namespace detail { + template + using GeneratedPhysicalRieszMethod = + typename DeclaredGeneratedPhysicalRieszCoordinate::Method; + + template + struct GeneratedPhysicalRieszRuntimeCoordinate : std::false_type { }; + + template + struct GeneratedPhysicalRieszRuntimeCoordinate< + Generated, + Kind, + std::void_t::topology)>> + : std::bool_constant< + DeclaredGeneratedPhysicalRieszCoordinate::registered && + GeneratedPhysicalRieszMethod::topology == + RieszTopology::global_scalar> { }; + + template + struct SpecificationPhysicalRieszRuntimeCoverage : std::false_type { }; + + template + struct GeneratedPhysicalRieszRuntimeCoverage : std::false_type { }; + + template + struct GeneratedPhysicalRieszRuntimeCoverage< + models::ModelTypeList, + models::ModelTypeList> + : std::bool_constant< + (GeneratedPhysicalRieszRuntimeCoordinate::value && ...) && + (GeneratedPhysicalRieszRuntimeCoordinate::value && ...)> { }; + + template + struct SpecificationPhysicalRieszRuntimeCoverage< + Specification, + std::void_t< + typename models::SpecificationContribution::GeneratedValues, + typename models::SpecificationContribution::GeneratedResiduals>> + : GeneratedPhysicalRieszRuntimeCoverage< + typename models::SpecificationContribution::GeneratedValues, + typename models::SpecificationContribution::GeneratedResiduals> { }; + + template + struct SpecificationSetPhysicalRieszRuntimeCoverage : std::false_type { }; + + template + struct SpecificationSetPhysicalRieszRuntimeCoverage< + models::detail::SpecificationSetStorage> + : std::bool_constant< + (SpecificationPhysicalRieszRuntimeCoverage::value && ...)> { }; + } // namespace detail + + template + concept CompleteGeneratedPhysicalRieszRuntimeNormalizationFor = + detail::SpecificationPhysicalRieszRuntimeCoverage< + std::remove_cvref_t>::value; + +#define MEAN_FIELD_PHYSICAL_RIESZ_TRAIT(BlockType, TopologyValue, ScaleValue) \ + template <> struct PhysicalRieszBlockTraits { \ + using Method = PhysicalRieszCoordinate; \ + 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 + [[nodiscard]] double physicalScale( + const StellarCharacteristicScales &scales + ) { + static_assert(PhysicalRieszBlockTraits::registered, "The block has no Physical Riesz normalization."); + using Method = typename PhysicalRieszBlockTraits::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 struct MakePhysicalRieszPlan; + + template + struct MakePhysicalRieszPlan< + utils::blocks::type_list, + utils::blocks::type_list> { + using Type = NormalizationPlan< + CoordinateComponent< + CoordinateKind::value, + utils::blocks::type_list, + typename PhysicalRieszBlockTraits::Method>..., + CoordinateComponent< + CoordinateKind::residual, + utils::blocks::type_list, + typename PhysicalRieszBlockTraits::Method>...>; + }; + } // namespace detail + + template + requires utils::blocks::block_form_is_valid_v + 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 struct NormalizationCompilation { + using Plan = NormalizationPlan<>; + static constexpr bool registered = false; + + template + 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 + requires utils::blocks::block_form_is_valid_v + struct RuntimePreparedNormalizationCompilation { + using Plan = RuntimePreparedNormalizationPlanFor; + static constexpr bool registered = CompleteNormalizationFor; + + template + static constexpr bool runtimeAvailableFor = registered; + }; + + template + requires utils::blocks::block_form_is_valid_v + struct NormalizationCompilation { + using Plan = IdentityNormalizationPlanFor; + static constexpr bool registered = CompleteNormalizationFor; + + template + static constexpr bool runtimeAvailableFor = registered; + }; + + template + requires utils::blocks::block_form_is_valid_v + struct NormalizationCompilation, Form> { + using Plan = PhysicalRieszNormalizationPlanFor; + static constexpr bool registered = CompleteNormalizationFor; + + template + static constexpr bool runtimeAvailableFor = + registered && + PhysicalRieszCoreRuntime> && + detail::SpecificationSetPhysicalRieszRuntimeCoverage< + std::remove_cvref_t>::value; + }; + + namespace detail { + template + struct NormalizationCompilationAudit { + using Plan = NormalizationPlan<>; + static constexpr bool registered = false; + }; + + template + requires NormalizationPrescription> && + utils::blocks::block_form_is_valid_v> + struct NormalizationCompilationAudit< + Prescription, + Form, + std::void_t< + typename NormalizationCompilation< + std::remove_cvref_t, + std::remove_cvref_t>::Plan, + decltype(std::bool_constant( + NormalizationCompilation< + std::remove_cvref_t, + std::remove_cvref_t>::registered)>{})>> { + using Compilation = NormalizationCompilation< + std::remove_cvref_t, + std::remove_cvref_t>; + using Plan = typename Compilation::Plan; + + static constexpr bool registered = + static_cast(Compilation::registered) && + CompleteNormalizationFor>; + }; + } // namespace detail + + template + using NormalizationPlanFor = typename detail::NormalizationCompilationAudit< + std::remove_cvref_t, + Form>::Plan; + + template + concept CompilableNormalizationFor = + detail::NormalizationCompilationAudit< + std::remove_cvref_t, + std::remove_cvref_t>::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 + concept RuntimePreparedNormalizationFor = + NormalizationPrescription> && + utils::blocks::block_form_is_valid_v> && + CompilableNormalizationFor< + std::remove_cvref_t, + std::remove_cvref_t> && + std::same_as< + NormalizationPlanFor< + std::remove_cvref_t, + std::remove_cvref_t>, + RuntimePreparedNormalizationPlanFor< + std::remove_cvref_t, + std::remove_cvref_t>>; + + 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::registered>, + decltype(std::bool_constant( + NormalizationCompilation< + Prescription, + Form>::template runtimeAvailableFor< + PhysicalCore, + SpecificationTypes>)>{})>> + : std::bool_constant< + (std::same_as || + PhysicalRieszDiagonalPrescription || + RuntimePreparedNormalizationFor) && + static_cast(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, + std::remove_cvref_t, + std::remove_cvref_t, + std::remove_cvref_t>::value; +} // namespace mean_field::normalization diff --git a/libmeanfield/interface/normalization/plan.cppm b/libmeanfield/interface/normalization/plan.cppm new file mode 100644 index 0000000..0b1c3ea --- /dev/null +++ b/libmeanfield/interface/normalization/plan.cppm @@ -0,0 +1,376 @@ +module; + +#include +#include + +export module mean_field:normalization.plan; + +export import :utils.blocks; + +export namespace mean_field::normalization { + struct NormalizationPrescriptionTag { }; + + template + concept NormalizationPrescription = + std::derived_from< + std::remove_cvref_t, + 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 + struct RuntimePreparedCoordinate final { + using PrescriptionType = std::remove_cvref_t; + }; + + template struct PhysicalRieszCoordinate final { + static constexpr RieszTopology topology = Topology; + static constexpr PhysicalScaleKind scale = Scale; + }; + + struct UnsupportedPhysicalRieszCoordinate final { }; + + template struct PhysicalRieszBlockTraits { + using Method = UnsupportedPhysicalRieszCoordinate; + static constexpr bool registered = false; + }; + + template + 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 struct IsTypeList : std::false_type { }; + + template + struct IsTypeList> : std::true_type { }; + + template struct IsUniqueDerivedBlockList : std::false_type { }; + + template + struct IsUniqueDerivedBlockList, Base> + : std::bool_constant< + (std::derived_from && ...) && + utils::blocks::types_are_unique_v>> { }; + + template struct IsCoordinateMethod : std::false_type { }; + + template <> struct IsCoordinateMethod : std::true_type { }; + + template + struct IsCoordinateMethod> + : std::true_type { }; + + template + struct IsCoordinateMethod> : std::true_type { }; + + template struct MethodSupportsBlock : std::false_type { }; + + template + struct MethodSupportsBlock + : std::bool_constant> { }; + + template + struct MethodSupportsBlock, Block> + : std::bool_constant> { }; + + template + struct MethodSupportsBlock, Block> + : std::bool_constant< + PhysicalRieszBlockTraits::registered && + std::same_as< + typename PhysicalRieszBlockTraits::Method, + PhysicalRieszCoordinate>> { }; + + template struct MethodSupportsEveryBlock : std::false_type { }; + + template + struct MethodSupportsEveryBlock> + : std::bool_constant<(MethodSupportsBlock::value && ...)> { }; + + template struct ComponentTraits { + static constexpr bool valid = false; + }; + + template + 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, CoordinateKind>; + + static constexpr bool hasValidBlockList = [] { + if constexpr (!hasValidKind || !IsTypeList::value) { + return false; + } else if constexpr (Candidate::kind == CoordinateKind::value) { + return IsUniqueDerivedBlockList::value; + } else if constexpr (Candidate::kind == CoordinateKind::residual) { + return IsUniqueDerivedBlockList::value; + } else { + return false; + } + }(); + + static constexpr bool hasCoherentCoordinateLists = [] { + if constexpr (!hasValidKind || !IsTypeList::value || + !IsTypeList::value) { + return false; + } else if constexpr (Candidate::kind == CoordinateKind::value) { + return std::same_as && + std::same_as>; + } else if constexpr (Candidate::kind == CoordinateKind::residual) { + return std::same_as> && + std::same_as; + } else { + return false; + } + }(); + + static constexpr bool valid = hasValidKind && IsTypeList::value && + IsCoordinateMethod::value && hasValidBlockList && + hasCoherentCoordinateLists && + MethodSupportsEveryBlock::value; + }; + + template struct Concatenate; + + template <> struct Concatenate<> { + using Type = utils::blocks::type_list<>; + }; + + template struct Concatenate> { + using Type = utils::blocks::type_list; + }; + + template + struct Concatenate, utils::blocks::type_list, Remaining...> { + using Type = typename Concatenate, Remaining...>::Type; + }; + + template using ConcatenateT = typename Concatenate::Type; + + template struct Append; + + template + struct Append, Appended> { + using Type = utils::blocks::type_list; + }; + + template using AppendT = typename Append::Type; + + template + using AppendUniqueT = std::conditional_t< + utils::blocks::contains_type_v, + List, + AppendT>; + + template struct ListDifference; + + template + struct ListDifference, Excluded> { + using Type = utils::blocks::type_list<>; + }; + + template + struct ListDifference, Excluded> { + private: + using Remaining = typename ListDifference, Excluded>::Type; + + public: + using Type = std::conditional_t< + utils::blocks::contains_type_v, + Remaining, + ConcatenateT, Remaining>>; + }; + + template + using ListDifferenceT = typename ListDifference::Type; + + template struct CollectRepeatedTypes; + + template + struct CollectRepeatedTypes, Original, Repeated> { + using Type = Repeated; + }; + + template + struct CollectRepeatedTypes, Original, Repeated> { + private: + using Next = std::conditional_t< + (utils::blocks::type_count_v > 1), + AppendUniqueT, + Repeated>; + + public: + using Type = typename CollectRepeatedTypes, Original, Next>::Type; + }; + + template + using RepeatedTypesT = typename CollectRepeatedTypes< + List, + List, + utils::blocks::type_list<>>::Type; + + template struct PlanTraits { + static constexpr bool valid = false; + }; + } // namespace detail + + template + concept NormalizationComponent = detail::ComponentTraits>::valid; + + template struct NormalizationPlan final { + using ComponentTypes = utils::blocks::type_list; + using ValueBlocks = detail::ConcatenateT; + using ResidualBlocks = detail::ConcatenateT; + }; + + namespace detail { + template + struct PlanTraits> { + static constexpr bool valid = (ComponentTraits::valid && ...); + }; + + template struct MakeIdentityPlan; + + template + struct MakeIdentityPlan< + utils::blocks::type_list, + utils::blocks::type_list> { + using Type = NormalizationPlan< + CoordinateComponent, IdentityCoordinate>..., + CoordinateComponent< + CoordinateKind::residual, + utils::blocks::type_list, + IdentityCoordinate>...>; + }; + + template < + NormalizationPrescription Prescription, + typename Values, + typename Residuals> + struct MakeRuntimePreparedPlan; + + template < + NormalizationPrescription Prescription, + typename... Values, + typename... Residuals> + struct MakeRuntimePreparedPlan< + Prescription, + utils::blocks::type_list, + utils::blocks::type_list> { + using Method = RuntimePreparedCoordinate; + using Type = NormalizationPlan< + CoordinateComponent< + CoordinateKind::value, + utils::blocks::type_list, + Method>..., + CoordinateComponent< + CoordinateKind::residual, + utils::blocks::type_list, + Method>...>; + }; + } // namespace detail + + template + concept NormalizationPlanType = detail::PlanTraits>::valid; + + template + requires utils::blocks::block_form_is_valid_v + using IdentityNormalizationPlanFor = typename detail::MakeIdentityPlan< + typename Form::value_blocks, + typename Form::residual_blocks>::Type; + + template + requires utils::blocks::block_form_is_valid_v + using RuntimePreparedNormalizationPlanFor = + typename detail::MakeRuntimePreparedPlan< + std::remove_cvref_t, + typename Form::value_blocks, + typename Form::residual_blocks>::Type; + + template + requires utils::blocks::block_form_is_valid_v + struct NormalizationCoverage final { + using DeclaredValueBlocks = typename Plan::ValueBlocks; + using DeclaredResidualBlocks = typename Plan::ResidualBlocks; + + using MissingValueBlocks = detail::ListDifferenceT; + using UnexpectedValueBlocks = detail::ListDifferenceT; + using RepeatedValueBlocks = detail::RepeatedTypesT; + + using MissingResidualBlocks = detail::ListDifferenceT; + using UnexpectedResidualBlocks = detail::ListDifferenceT; + using RepeatedResidualBlocks = detail::RepeatedTypesT; + + 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 + concept CompleteNormalizationFor = utils::blocks::block_form_is_valid_v && + NormalizationPlanType && + NormalizationCoverage>::complete; +} // namespace mean_field::normalization diff --git a/libmeanfield/interface/normalization/stellar_equilibrium.cppm b/libmeanfield/interface/normalization/stellar_equilibrium.cppm new file mode 100644 index 0000000..2f0b82e --- /dev/null +++ b/libmeanfield/interface/normalization/stellar_equilibrium.cppm @@ -0,0 +1,921 @@ +module; + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +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 *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 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 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 marker(finiteElements.mesh->bdr_attributes.Max()); + marker = 0; + constexpr int attribute = DomainSchema::template boundary_attribute(); + 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 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 + [[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 + using PhysicalRieszMethodFor = typename PhysicalRieszBlockTraits::Method; + + template + struct IsGlobalGeneratedValueNormalization : std::false_type { }; + + template + struct IsGlobalGeneratedValueNormalization< + utils::blocks::generated_value_block, + std::void_t< + decltype(PhysicalRieszMethodFor< + utils::blocks::generated_value_block>::topology), + decltype(PhysicalRieszMethodFor< + utils::blocks::generated_value_block>::scale)>> + : std::bool_constant< + PhysicalRieszBlockTraits< + utils::blocks::generated_value_block>::registered && + PhysicalRieszMethodFor< + utils::blocks::generated_value_block>::topology == + RieszTopology::global_scalar> { }; + + template + struct GeneratedValueBlocksBelongToSpecification : std::false_type { }; + + template + struct GeneratedCoordinateBelongsToSpecification : std::false_type { }; + + template + struct GeneratedCoordinateBelongsToSpecification< + Generated, + Specification, + std::void_t> + : std::bool_constant< + std::same_as> { }; + + template + struct GeneratedValueBlocksBelongToSpecification< + utils::blocks::type_list...>, + Specification> + : std::bool_constant< + (GeneratedCoordinateBelongsToSpecification::value && ...)> { }; + + template + struct IsGlobalGeneratedResidualNormalization : std::false_type { }; + + template + struct IsGlobalGeneratedResidualNormalization< + utils::blocks::generated_residual_block, + std::void_t< + decltype(PhysicalRieszMethodFor< + utils::blocks::generated_residual_block>::topology), + decltype(PhysicalRieszMethodFor< + utils::blocks::generated_residual_block>::scale)>> + : std::bool_constant< + PhysicalRieszBlockTraits< + utils::blocks::generated_residual_block>::registered && + PhysicalRieszMethodFor< + utils::blocks::generated_residual_block>::topology == + RieszTopology::global_scalar> { }; + + template + struct GeneratedResidualBlocksBelongToSpecification : std::false_type { }; + + template + struct GeneratedResidualBlocksBelongToSpecification< + utils::blocks::type_list...>, + Specification> + : std::bool_constant< + (GeneratedCoordinateBelongsToSpecification::value && ...)> { }; + + template struct PrepareGeneratedValueNormalizations { + static constexpr bool registered = false; + + template + static constexpr bool completeFor = false; + + template + static void Apply( + DiagonalNormalizationBuilder &, + const StellarCharacteristicScales & + ) { + static_assert(registered, "Generated value-block normalization metadata is malformed."); + } + }; + + template + struct PrepareGeneratedValueNormalizations> { + static constexpr bool registered = + (IsGlobalGeneratedValueNormalization::value && ...); + + template + static constexpr bool completeFor = registered && + utils::blocks::block_form_is_valid_v && + (utils::blocks::contains_type_v && ...); + + template + static void Apply( + DiagonalNormalizationBuilder &builder, + const StellarCharacteristicScales &scales + ) { + if constexpr (completeFor) { + (builder.template SetValueGlobal(physicalScale(scales)), ...); + } else { + static_assert( + completeFor, + "Every generated value block must have a declared global-scalar Physical Riesz law " + "and belong to the compiled equilibrium form." + ); + } + } + }; + + template struct PrepareGeneratedResidualNormalizations { + static constexpr bool registered = false; + + template + static constexpr bool completeFor = false; + + template + static void Apply( + DiagonalNormalizationBuilder &, + const StellarCharacteristicScales & + ) { + static_assert(registered, "Generated residual-block normalization metadata is malformed."); + } + }; + + template + struct PrepareGeneratedResidualNormalizations> { + static constexpr bool registered = + (IsGlobalGeneratedResidualNormalization::value && ...); + + template + static constexpr bool completeFor = registered && + utils::blocks::block_form_is_valid_v && + (utils::blocks::contains_type_v && ...); + + template + static void Apply( + DiagonalNormalizationBuilder &builder, + const StellarCharacteristicScales &scales + ) { + if constexpr (completeFor) { + (builder.template SetResidualGlobal(physicalScale(scales)), ...); + } else { + static_assert( + completeFor, + "Every generated residual block must have a declared global-scalar Physical Riesz law " + "and belong to the compiled equilibrium form." + ); + } + } + }; + + template + struct CompileStellarSpecificationNormalization { + using ValuePreparation = PrepareGeneratedValueNormalizations; + using ResidualPreparation = PrepareGeneratedResidualNormalizations; + + static constexpr bool registered = false; + + template + static constexpr bool completeFor = false; + + template + static void Apply( + DiagonalNormalizationBuilder &, + const StellarCharacteristicScales & + ) { + static_assert( + completeFor, + "The specification has no complete generated-coordinate normalization." + ); + } + }; + + template + struct CompileStellarSpecificationNormalization< + Specification, + std::void_t< + typename operators::StellarEquilibriumSpecificationCompilation< + Specification>::GeneratedValueBlocks, + typename operators::StellarEquilibriumSpecificationCompilation< + Specification>::GeneratedResidualBlocks>> { + using OperatorCompilation = + operators::StellarEquilibriumSpecificationCompilation; + 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 + static constexpr bool completeFor = registered && + ValuePreparation::template completeFor && + ResidualPreparation::template completeFor; + + template + static void Apply( + DiagonalNormalizationBuilder &builder, + const StellarCharacteristicScales &scales + ) { + if constexpr (completeFor) { + ValuePreparation::template Apply(builder, scales); + ResidualPreparation::template Apply(builder, scales); + } else { + static_assert( + completeFor, + "The specification's generated blocks do not have a complete runtime normalization." + ); + } + } + }; + + template struct PrepareSpecificationNormalizations; + + template + struct PrepareSpecificationNormalizations> { + static constexpr bool registered = + (CompileStellarSpecificationNormalization::registered && ...); + + template + static constexpr bool completeFor = + (CompileStellarSpecificationNormalization::template completeFor && ...); + + template + static void Apply( + DiagonalNormalizationBuilder &builder, + const StellarCharacteristicScales &scales + ) { + static_assert( + completeFor, + "Every generated stellar-equilibrium coordinate requires a declared global-scalar " + "Physical Riesz normalization and compiler-owned root block." + ); + (CompileStellarSpecificationNormalization::template Apply(builder, scales), ...); + } + }; + + template + struct StellarModelNormalizationCoverage : std::false_type { }; + + template + requires model::StellarModelType && utils::blocks::block_form_is_valid_v + struct StellarModelNormalizationCoverage< + Model, + Form, + std::void_t::SpecificationTypes>> + : std::bool_constant< + PrepareSpecificationNormalizations< + typename std::remove_cvref_t::SpecificationTypes>::template completeFor> { }; + } // namespace detail + + template + struct StellarSpecificationNormalizationContribution + : detail::CompileStellarSpecificationNormalization> { + using Base = detail::CompileStellarSpecificationNormalization>; + + template + static void Apply( + DiagonalNormalizationBuilder &builder, + const StellarCharacteristicScales &scales + ) { + static_assert( + Base::template completeFor, + "The specification's generated blocks do not have a complete runtime normalization." + ); + Base::template Apply(builder, scales); + } + }; + + template + concept RegisteredStellarSpecificationNormalization = + StellarSpecificationNormalizationContribution::registered; + + template + concept CompleteStellarSpecificationNormalizationFor = + utils::blocks::block_form_is_valid_v && + StellarSpecificationNormalizationContribution::template completeFor; + + template + concept CompleteStellarNormalizationFor = + detail::StellarModelNormalizationCoverage< + std::remove_cvref_t, + std::remove_cvref_t>::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 + concept PhysicalRieszStellarEquilibriumCore = + operators::PreparedStellarEquilibriumPhysicalCore> && + PhysicalRieszCoreRuntime>; + + template + concept PhysicalRieszStellarEquilibriumProblem = + equilibrium::DiscretizedStellarEquilibriumProblem> && + requires { + typename std::remove_cvref_t::ModelType; + typename std::remove_cvref_t::FormType; + typename std::remove_cvref_t::PhysicalCoreType; + typename std::remove_cvref_t::NormalizationPrescriptionType; + requires PhysicalRieszDiagonalPrescription< + typename std::remove_cvref_t::NormalizationPrescriptionType>; + requires CompilableNormalizationFor< + typename std::remove_cvref_t::NormalizationPrescriptionType, + typename std::remove_cvref_t::FormType>; + requires CompleteStellarNormalizationFor< + typename std::remove_cvref_t::ModelType, + typename std::remove_cvref_t::FormType>; + requires StellarNormalizationRuntimeAvailableFor< + typename std::remove_cvref_t::NormalizationPrescriptionType, + typename std::remove_cvref_t::FormType, + typename std::remove_cvref_t::PhysicalCoreType, + typename std::remove_cvref_t::ModelType::SpecificationTypes>; + }; + + template + requires std::same_as< + typename std::remove_cvref_t::NormalizationPrescriptionType, + Unnormalized> + [[nodiscard]] DiagonalNormalization prepareNormalization(const Problem &problem) { + return DiagonalNormalization::Identity(problem.StateSize(), problem.EquationSize()); + } + + template + [[nodiscard]] DiagonalNormalization prepareNormalization(const Problem &problem) { + using ProblemType = std::remove_cvref_t; + 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 stellarMarker = + utils::domain::make_attribute_marker(*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(*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 builder(problem.GetManifest().layout()); + builder.template SetValueBlock( + physicalScale(scales), densityDiagonal + ); + builder.template SetValueBlock( + physicalScale(scales), surfaceDiagonal + ); + builder.template SetValueBlock( + physicalScale(scales), gravityGradientDiagonal + ); + builder.template SetValueBlock( + physicalScale(scales), gravityPotentialDiagonal + ); + builder.template SetValueBlock( + physicalScale(scales), enthalpyDiagonal + ); + builder.template SetResidualBlock( + physicalScale(scales), gravityGradientDiagonal + ); + builder.template SetResidualBlock( + physicalScale(scales), gravityPotentialDiagonal + ); + builder.template SetResidualBlock( + physicalScale(scales), densityDiagonal + ); + builder.template SetResidualBlock( + physicalScale(scales), surfaceDiagonal + ); + const mfem::Array &surfaceRows = problem.GetPressureSurfaceRows().reduced_dofs(); + builder.template SetHybridResidualBlock( + physicalScale(scales), + enthalpyDiagonal, + std::span{surfaceRows.GetData(), static_cast(surfaceRows.Size())} + ); + detail::PrepareSpecificationNormalizations::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 + requires( + !std::same_as< + typename std::remove_cvref_t::NormalizationPrescriptionType, + Unnormalized> && + !PhysicalRieszDiagonalPrescription< + typename std::remove_cvref_t::NormalizationPrescriptionType> && + RuntimePreparedNormalizationOperation) + [[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 + concept NormalizableStellarEquilibriumProblem = + equilibrium::DiscretizedStellarEquilibriumProblem> && + requires(const std::remove_cvref_t &problem) { + { + prepareNormalization(problem) + } -> std::same_as; + }; + + 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 + concept ProblemBoundStellarInverseFor = + NormalizableStellarEquilibriumProblem> && + std::derived_from, mfem::Solver> && + requires(const std::remove_cvref_t &inverse) { + { + inverse.GetProblem() + } -> std::same_as &>; + { + inverse.IsCurrent() + } -> std::same_as; + }; + + template + requires ProblemBoundStellarInverseFor + 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 + class NormalizedStellarEquilibriumOperator final : public mfem::Operator { + private: + using ProblemType = std::remove_cvref_t; + + 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 + requires ProblemBoundStellarInverseFor + [[nodiscard]] NormalizedStellarPreconditioner> + 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 + requires ProblemBoundStellarInverseFor + class NormalizedStellarPreconditioner final : public mfem::Solver { + private: + using ProblemType = std::remove_cvref_t; + using NormalizedOperator = NormalizedStellarEquilibriumOperator; + using PhysicalInverseType = std::remove_cvref_t; + + [[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 + template + requires ProblemBoundStellarInverseFor + NormalizedStellarPreconditioner> + NormalizedStellarEquilibriumOperator::MakeScaledPreconditioner(PhysicalInverse &physicalInverse) const { + VerifyPrepared(); + return NormalizedStellarPreconditioner>{ + *this, + physicalInverse + }; + } + + template + [[nodiscard]] auto makeNormalizedStellarEquilibriumOperator(Problem &problem) { + return NormalizedStellarEquilibriumOperator{problem}; + } +} // namespace mean_field::normalization diff --git a/libmeanfield/interface/operators/prepared_angular_momentum.cppm b/libmeanfield/interface/operators/prepared_angular_momentum.cppm new file mode 100644 index 0000000..ced565c --- /dev/null +++ b/libmeanfield/interface/operators/prepared_angular_momentum.cppm @@ -0,0 +1,196 @@ +module; + +#include +#include +#include + +#include + +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 densityDofs; + mfem::Array displacementDofs; + mfem::Array compactificationDofs; + mfem::DofTransformation *densityDofTransformation{nullptr}; + mfem::DofTransformation *displacementDofTransformation{nullptr}; + mfem::DofTransformation *compactificationDofTransformation{nullptr}; + mfem::Vector baseDisplacement; + mfem::Vector compactification; + std::vector 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 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 diff --git a/libmeanfield/interface/operators/prepared_central_density_stellar_equilibrium.cppm b/libmeanfield/interface/operators/prepared_central_density_stellar_equilibrium.cppm deleted file mode 100644 index b189395..0000000 --- a/libmeanfield/interface/operators/prepared_central_density_stellar_equilibrium.cppm +++ /dev/null @@ -1,117 +0,0 @@ -module; - -#include -#include -#include -#include - -#include - -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>; - - 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; - 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( - 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 physicalOperator, - models::CompiledFixedCentralDensity centralDensity, - field::FieldPointDofMap centerDof - ); - - void AssembleResidual(); - void VerifyPrepared() const; - - std::unique_ptr m_physicalOperator; - models::CompiledFixedCentralDensity m_centralDensity; - PreparedCentralDensityConstraint m_phaseConstraint; - CentralDensityStellarEquilibriumRootManifest m_rootManifest; - mfem::Vector m_cachedResidual; - bool m_isPrepared{false}; - }; -} // namespace mean_field::operators diff --git a/libmeanfield/interface/operators/prepared_hydrostatic_equilibrium_operator.cppm b/libmeanfield/interface/operators/prepared_hydrostatic_equilibrium_operator.cppm index 43746ba..8cb41b6 100644 --- a/libmeanfield/interface/operators/prepared_hydrostatic_equilibrium_operator.cppm +++ b/libmeanfield/interface/operators/prepared_hydrostatic_equilibrium_operator.cppm @@ -35,6 +35,7 @@ export namespace mean_field::operators { std::uint64_t enthalpyApplications{0}; std::uint64_t gravityPotentialApplications{0}; std::uint64_t bernoulliConstantApplications{0}; + std::uint64_t rotationAmplitudeApplications{0}; std::uint64_t combinedApplications{0}; constexpr auto operator<=>(const PreparedHydrostaticAlgebraicJacobianStatistics &) const = default; @@ -118,6 +119,14 @@ export namespace mean_field::operators { mfem::Vector &action ) 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( const mfem::Vector &enthalpyVariation, const mfem::Vector &gravityPotentialVariation, diff --git a/libmeanfield/interface/operators/prepared_stellar_equilibrium.cppm b/libmeanfield/interface/operators/prepared_stellar_equilibrium.cppm index 8a8b0df..7aabf56 100644 --- a/libmeanfield/interface/operators/prepared_stellar_equilibrium.cppm +++ b/libmeanfield/interface/operators/prepared_stellar_equilibrium.cppm @@ -96,6 +96,20 @@ export namespace mean_field::operators { class PreparedStellarEquilibriumOperator final : public mfem::Operator { 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 requires std::same_as< typename std::remove_cvref_t::EquationOfStateType, @@ -174,6 +188,12 @@ export namespace mean_field::operators { [[nodiscard]] const PreparedHydrostaticEquilibriumOperator &GetHydrostaticOperator() const noexcept; [[nodiscard]] const PreparedDisplacementResidualOperator &GetDisplacementOperator() 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 deformation::PreparedDomainDeformationRuntime &GetDomainDeformation() const noexcept; [[nodiscard]] const mfem::Vector &GetSurfaceDeformationParameters() const; @@ -234,5 +254,6 @@ export namespace mean_field::operators { mutable mfem::Vector m_fullMechanicalAction; mutable mfem::Vector m_surfaceShapeAction; mutable mfem::Vector m_pullbackDerivativeAction; + mutable mfem::Vector m_densityVolumeIntegralAction; }; } // namespace mean_field::operators diff --git a/libmeanfield/interface/operators/prepared_variadic_stellar_equilibrium.cppm b/libmeanfield/interface/operators/prepared_variadic_stellar_equilibrium.cppm new file mode 100644 index 0000000..baa01d0 --- /dev/null +++ b/libmeanfield/interface/operators/prepared_variadic_stellar_equilibrium.cppm @@ -0,0 +1,3163 @@ +module; + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +export module mean_field:operators.prepared_variadic_stellar_equilibrium; + +export import :operators.prepared_angular_momentum; +export import :operators.prepared_central_density; +export import :operators.prepared_stellar_equilibrium; +export import :operators.stellar_equilibrium_compiler; + +/* + * A physics-authored residual or derivative provider must make one of two + * explicit statements for every row/edge inferred from Reads/Changes: + * + * - return the token produced by row.add(...); or + * - return structuralZero when the declared edge is identically zero. + * + * The outer runtime, rather than the extension, enumerates the compiler's + * complete incidence set. These tiny result types let that enumeration + * distinguish an intentional mathematical zero from an accidentally empty + * hook without exposing any backend block machinery to a physics author. + */ +export namespace mean_field::stellar { + struct ContributionAdded final { }; + struct StructuralZero final { }; + + inline constexpr StructuralZero structuralZero{}; + inline constexpr StructuralZero zeroDerivative{}; + + template + concept ContributionResult = + std::same_as, ContributionAdded> || + std::same_as, StructuralZero>; +} // namespace mean_field::stellar + +export namespace mean_field::operators { + /** + * Capability boundary for the coupled finite-element physics core. + * Describing an EOS is intentionally easier than implementing its + * finite-element runtime core. Surface equations are compiled and + * prepared by their own specification contribution, so this backend is + * selected solely by the constitutive law. + */ + template + struct StellarEquilibriumCoreRuntime { + static constexpr bool registered = false; + }; + + template <> + struct StellarEquilibriumCoreRuntime { + static constexpr bool registered = true; + using CoreType = PreparedStellarEquilibriumOperator; + + [[nodiscard]] static std::unique_ptr Make( + fem::FEM &finiteElements, + const mapping::DomainMapper &domainMapper, + const eos::Polytrope &equationOfState, + const models::CompiledFixedMass &fixedMass, + PressureSurfaceConstraintView surfaceConstraint, + deformation::PreparedDomainDeformationRuntime domainDeformation + ) { + return std::make_unique( + finiteElements, + domainMapper, + equationOfState, + fixedMass, + surfaceConstraint, + std::move(domainDeformation) + ); + } + + [[nodiscard]] static int SurfaceEquationCount(const CoreType &core) noexcept { + return static_cast(core.GetSurfaceConstraintOperator().GetSurfaceRows().size()); + } + }; + + /** + * Minimal common protocol consumed by the variadic outer root. + * + * EOS backends may use different concrete core types. They only need to + * implement this numerical protocol and expose that type as + * StellarEquilibriumCoreRuntime::CoreType. Specification runtimes + * are audited separately against the selected concrete core, so a + * constraint that needs additional physical facilities is rejected at its + * own compile-time boundary. + */ + template + concept PreparedStellarEquilibriumPhysicalCore = + std::derived_from, mfem::Operator> && + requires( + std::remove_cvref_t &core, + const std::remove_cvref_t &constantCore, + const mfem::Vector &state, + mfem::Vector &residual, + const StellarEquilibriumDependencies &dependencies, + const physics::RigidRotation &rotation + ) { + { + constantCore.GetLayout() + } -> std::same_as; + { + core.Prepare(state, dependencies, rotation) + } -> std::same_as; + { + constantCore.BuildResidual(residual) + } -> std::same_as; + { + constantCore.IsPrepared() + } -> std::convertible_to; + { + constantCore.GetFixedMassReport() + } -> std::same_as; + { + constantCore.GetDependencies() + } -> std::same_as; + { + constantCore.GetGeneratedDisplacementDependency() + } -> std::same_as; + { + constantCore.GetSurfaceConstraintOperator() + } -> std::same_as; + }; + + namespace detail { + template + struct BackendSpecificationListTraits final { + static constexpr bool valid = false; + + template + static constexpr bool contains = false; + }; + + template + struct BackendSpecificationListTraits< + models::ModelTypeList> final { + static constexpr bool valid = + (models::ModelSpecification && ...) && + utils::blocks::types_are_unique_v< + utils::blocks::type_list>; + + template + static constexpr bool contains = + (std::same_as, Specifications> || ...); + }; + + template + struct CoreRuntimeInterfaceAudit { + using CoreType = void; + static constexpr bool complete = false; + }; + + template + struct CoreRuntimeInterfaceAudit< + Model, + std::void_t< + typename StellarEquilibriumCoreRuntime< + typename std::remove_cvref_t::EquationOfStateType>::CoreType, + typename StellarEquilibriumCoreRuntime< + typename std::remove_cvref_t::EquationOfStateType>:: + CoreType::BackendSpecifications, + std::bool_constant( + StellarEquilibriumCoreRuntime< + typename std::remove_cvref_t::EquationOfStateType>::registered)>>> { + private: + using ModelType = std::remove_cvref_t; + using EquationOfState = typename ModelType::EquationOfStateType; + using Runtime = StellarEquilibriumCoreRuntime; + + public: + using CoreType = typename Runtime::CoreType; + + static constexpr bool complete = + BackendSpecificationListTraits< + typename CoreType::BackendSpecifications>::valid && + PreparedStellarEquilibriumPhysicalCore && + requires( + fem::FEM &finiteElements, + const mapping::DomainMapper &domainMapper, + const EquationOfState &equationOfState, + const models::CompiledFixedMass &fixedMass, + PressureSurfaceConstraintView surfaceConstraint, + deformation::PreparedDomainDeformationRuntime domainDeformation, + const CoreType &core + ) { + requires Runtime::registered; + { + Runtime::Make( + finiteElements, + domainMapper, + equationOfState, + fixedMass, + surfaceConstraint, + std::move(domainDeformation) + ) + } -> std::same_as>; + { + Runtime::SurfaceEquationCount(core) + } -> std::convertible_to; + }; + }; + } // namespace detail + + /** + * Structural contract for the privileged specification list owned by an + * EOS/core backend. Ordinary EOS, surface, and constraint authors do not + * use this facility; their nested EquilibriumPhysics package remains the + * restricted, astronomy-facing extension path. + */ + template + concept StellarEquilibriumBackendSpecificationList = + detail::BackendSpecificationListTraits< + std::remove_cvref_t>::valid; + + /** + * Backend runtime extension point for one physical model specification. + * + * The prepared stellar root is assembled by folding this trait over the + * model's canonical specification list. A new specification therefore + * contributes one prepared slot; no specialization for a *combination* of + * specifications is ever required. New physics-facing specifications + * should prefer their nested EquilibriumPhysics package below; explicit + * specializations remain the library/backend registry mechanism, but are + * selected only when the concrete core owner lists that exact + * specification in CoreType::BackendSpecifications. + */ + template + struct StellarEquilibriumRuntimeContribution { + static constexpr bool registered = false; + static constexpr std::size_t rotationProviders = 0; + }; + + template < + template typename PreparedImplementation, + std::size_t RotationProviderCount = 0> + struct PreparedStellarEquilibriumContribution { + static constexpr bool registered = true; + static constexpr std::size_t rotationProviders = RotationProviderCount; + + template + using Prepared = PreparedImplementation; + }; + + /* Physics-facing declaration for a specification's residual/Jacobian + * runtime. A constraint may expose + * + * using EquilibriumPhysics = + * operators::SpecificationEquilibriumPhysics; + * + * inside its own class. Unlike an explicit + * StellarEquilibriumRuntimeContribution specialization, this declaration + * is adapted through restricted physics-facing views. The implementation + * receives its exact specification at construction and never receives the + * full model, FEM backend, domain mapper, dependency set, or physical core. + * Explicit registry specializations remain a candidate backend extension + * point for built-in physics which must coordinate core internals. The + * selected concrete core's non-extendable BackendSpecifications member is what + * grants that candidate privileged access. */ + template < + template typename PreparedImplementation, + std::size_t RotationProviderCount = 0> + struct SpecificationEquilibriumPhysics final { + static_assert( + RotationProviderCount <= 1, + "One specification runtime can provide at most one rigid-rotation control." + ); + + static constexpr bool registered = true; + static constexpr std::size_t rotationProviders = RotationProviderCount; + + template + using Physics = PreparedImplementation; + }; + + namespace detail { + template + struct BindLocalSpecificationEquilibriumPhysics final { + template + using Physics = LocalPhysics; + }; + } // namespace detail + + /** + * Convenience spelling for ordinary specification-local physics. + * + * Most constraint implementations depend only on their exact + * specification and the restricted block views supplied by the adapter; + * they do not need the complete Model type. This alias binds such a + * concrete class into SpecificationEquilibriumPhysics without introducing + * a second runtime or duplicating any adapter logic. + */ + template + using LocalSpecificationEquilibriumPhysics = SpecificationEquilibriumPhysics< + detail::BindLocalSpecificationEquilibriumPhysics::template Physics>; + + namespace detail { + template + concept HasNestedStellarEquilibriumPhysics = requires { + typename std::remove_cvref_t::EquilibriumPhysics; + }; + + template + struct BackendRuntimeContributionAuthorization : std::false_type { }; + + template < + models::ModelSpecification Specification, + model::StellarModelType Model> + struct BackendRuntimeContributionAuthorization< + Specification, + Model, + std::void_t::EquationOfStateType>:: + CoreType::BackendSpecifications>> final + : std::bool_constant< + CoreRuntimeInterfaceAudit>::complete && + std::remove_cvref_t::template containsSpecification< + std::remove_cvref_t> && + BackendSpecificationListTraits::EquationOfStateType>:: + CoreType::BackendSpecifications>::template contains< + std::remove_cvref_t>> { }; + + template < + typename Specification, + typename Model, + typename = void> + struct BackendRuntimeContributionCandidate final { + static constexpr bool available = false; + static constexpr bool registered = false; + static constexpr std::size_t rotationProviders = 0; + }; + + template < + models::ModelSpecification Specification, + model::StellarModelType Model> + struct BackendRuntimeContributionCandidate< + Specification, + Model, + std::void_t< + std::enable_if_t::value>, + typename StellarEquilibriumRuntimeContribution< + Specification>::template Prepared, + std::bool_constant( + StellarEquilibriumRuntimeContribution< + Specification>::registered)>, + std::integral_constant< + std::size_t, + static_cast( + StellarEquilibriumRuntimeContribution< + Specification>::rotationProviders)>>> final { + using Contribution = StellarEquilibriumRuntimeContribution; + using Prepared = typename Contribution::template Prepared; + + static constexpr bool available = true; + static constexpr bool registered = Contribution::registered; + static constexpr std::size_t rotationProviders = + Contribution::rotationProviders; + }; + + template < + models::ModelSpecification Specification, + model::StellarModelType Model, + typename Physics> + class PhysicsFacingSpecificationRuntime; + + template < + models::ModelSpecification Specification, + model::StellarModelType Model, + bool HasNestedPhysics = HasNestedStellarEquilibriumPhysics, + typename = void> + struct RuntimeContributionSelection { + static constexpr bool available = false; + static constexpr bool ambiguous = false; + static constexpr std::size_t rotationProviders = 0; + }; + + /* A core-authorized explicit registry specialization is trusted + * backend code. Its established protocol deliberately retains direct + * access to the FEM, mapper, physical core, model, and dependency + * stamps. A specialization alone is ignored, so it cannot confer that + * privilege on an external specification paired with an existing + * core. */ + template + struct RuntimeContributionSelection< + Specification, + Model, + false, + std::void_t< + typename BackendRuntimeContributionCandidate< + Specification, + Model>::Prepared>> { + using Candidate = BackendRuntimeContributionCandidate; + using Contribution = typename Candidate::Contribution; + using Prepared = typename Candidate::Prepared; + + static constexpr bool available = Candidate::available; + static constexpr bool registered = Candidate::registered; + static constexpr bool ambiguous = false; + static constexpr std::size_t rotationProviders = + Candidate::rotationProviders; + }; + + /* A nested package is the safe physics-author path. The adapter owns + * all interaction with backend objects and forwards only restricted + * views to the implementation. Malformed packages remain detection + * safe through this partial specialization. */ + template + struct RuntimeContributionSelection< + Specification, + Model, + true, + std::void_t< + typename Specification::EquilibriumPhysics, + std::bool_constant( + Specification::EquilibriumPhysics::registered)>, + std::integral_constant< + std::size_t, + static_cast( + Specification::EquilibriumPhysics::rotationProviders)>, + typename Specification::EquilibriumPhysics::template Physics>> { + using Contribution = typename Specification::EquilibriumPhysics; + using Physics = typename Contribution::template Physics; + using Prepared = PhysicsFacingSpecificationRuntime; + + static constexpr bool available = true; + static constexpr bool registered = Contribution::registered; + static constexpr bool ambiguous = + BackendRuntimeContributionCandidate< + Specification, + Model>::registered; + static constexpr std::size_t rotationProviders = Contribution::rotationProviders; + }; + + template + using PreparedRuntimeContribution = + typename RuntimeContributionSelection::Prepared; + } // namespace detail + + /** + * Whether the selected concrete core explicitly permits one specification + * to use the privileged aggregate runtime registry. This is intentionally + * false for an otherwise valid external specification paired with the + * built-in core; such a specification must use its restricted nested + * EquilibriumPhysics package. + */ + template + inline constexpr bool stellarEquilibriumBackendRuntimeAuthorized = + detail::BackendRuntimeContributionAuthorization< + std::remove_cvref_t, + std::remove_cvref_t>::value; + + struct EmptySpecificationPreparationReport final { + [[nodiscard]] constexpr bool DidAnyWork() const noexcept { + return false; + } + }; + + namespace detail { + struct StellarEquilibriumControlContext final { + StellarEquilibriumDependencies dependencies; + std::optional rotation; + std::size_t rotationProviderCount{0}; + bool generatedPhysicalControl{false}; + }; + + /* + * Additional facilities used specifically by FixedAngularMomentum. + * This is deliberately a constraint-local protocol: an EOS core can + * support the base root without implementing these operations, and is + * rejected only when this physical constraint is selected. + */ + template + concept FixedAngularMomentumPhysicalCore = + PreparedStellarEquilibriumPhysicalCore && + requires(const std::remove_cvref_t &core) { + { + core.GetGravityContext() + } -> std::same_as; + { + core.GetDomainDeformation() + } -> std::same_as; + { + core.GetBarotropicClosureOperator() + } -> std::same_as; + { + core.GetHydrostaticOperator() + } -> std::same_as; + { + core.GetSurfaceConstraintOperator() + } -> std::same_as; + { + core.GetDisplacementOperator() + } -> std::same_as; + { + core.GetSurfaceDeformationParameters() + } -> std::same_as; + { + core.GetGeneratedDisplacementDependency() + } -> std::same_as; + }; + + template + class EmbeddedSpecificationRuntime final { + public: + using Report = EmptySpecificationPreparationReport; + + template + EmbeddedSpecificationRuntime( + fem::FEM &, + const mapping::DomainMapper &, + PhysicalCore &, + const Model & + ) noexcept { + } + + template + void ReadPhysicalControls(const StateView &, StellarEquilibriumControlContext &) noexcept { + } + + template + [[nodiscard]] Report PrepareAfterPhysical( + const StateView &, + const StellarEquilibriumDependencies &, + const PhysicalCore & + ) noexcept { + return {}; + } + + template + void AddResidual(const ResidualView &) const noexcept { + } + + template < + typename DirectionView, + typename ActionView, + PreparedStellarEquilibriumPhysicalCore PhysicalCore> + void AddJacobianAction( + const DirectionView &, + const ActionView &, + const PhysicalCore & + ) const noexcept { + } + + [[nodiscard]] constexpr bool IsPrepared() const noexcept { + return true; + } + }; + + struct FixedAngularMomentumPreparationReport final { + PreparedAngularMomentumReport constraint; + bool generatedRotation{false}; + + [[nodiscard]] bool DidAnyWork() const noexcept { + return constraint.DidAnyWork() || generatedRotation; + } + }; + + template + class FixedAngularMomentumRuntime final { + public: + using Report = FixedAngularMomentumPreparationReport; + + template + FixedAngularMomentumRuntime( + fem::FEM &finiteElements, + const mapping::DomainMapper &domainMapper, + PhysicalCore &physical, + const Model &model + ) + : m_constraint( + finiteElements, + domainMapper, + physical.GetGravityContext(), + models::compileConstraint( + model.template specification() + ) + ), + m_volumeDisplacementDirection(physical.GetDomainDeformation().volumeDisplacementSize()), + m_rotationalAngularVelocityAction(physical.GetDomainDeformation().volumeDisplacementSize()), + m_surfaceAngularVelocityAction(physical.GetDomainDeformation().parameterCount()), + m_hydrostaticAngularVelocityAction(physical.GetBarotropicClosureOperator().GetEnthalpySize()), + m_zeroEnthalpy(physical.GetBarotropicClosureOperator().GetEnthalpySize()) { + m_generatedRotationDependency.identity = + static_cast(reinterpret_cast(this)); + m_zeroEnthalpy = 0.0; + } + + template + void ReadPhysicalControls( + const StateView &state, + StellarEquilibriumControlContext &context + ) { + const auto angularVelocity = + state.block(utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term); + MFEM_VERIFY( + angularVelocity.Size() == 1 && std::isfinite(angularVelocity(0)), + "FixedAngularMomentum must generate one finite angular-velocity coordinate." + ); + + m_generatedRotationChanged = !m_isPrepared || angularVelocity(0) != m_angularVelocity; + if (m_generatedRotationChanged) { + m_angularVelocity = angularVelocity(0); + ++m_generatedRotationDependency.revision; + } + context.dependencies.rotation = m_generatedRotationDependency; + context.rotation = m_constraint.GetCompiledConstraint().makeRotation(m_angularVelocity); + ++context.rotationProviderCount; + context.generatedPhysicalControl = true; + } + + template + [[nodiscard]] Report PrepareAfterPhysical( + const StateView &, + const StellarEquilibriumDependencies &dependencies, + const PhysicalCore &physical + ) { + const StellarEquilibriumDependencyStamp &displacement = + physical.GetGeneratedDisplacementDependency(); + const auto constraintReport = m_constraint.Prepare( + m_angularVelocity, + {.discretization = { + .identity = dependencies.discretization.identity, + .revision = dependencies.discretization.revision + }, + .density = { + .identity = dependencies.density.identity, + .revision = dependencies.density.revision + }, + .displacement = { + .identity = displacement.identity, + .revision = displacement.revision + }, + .rotation = { + .identity = dependencies.rotation.identity, + .revision = dependencies.rotation.revision + }} + ); + m_isPrepared = true; + return {.constraint = constraintReport, .generatedRotation = m_generatedRotationChanged}; + } + + template + void AddResidual(const ResidualView &residual) const { + mfem::Vector constraintResidual; + m_constraint.BuildResidual(constraintResidual); + residual.add( + utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term, + constraintResidual + ); + } + + template < + typename DirectionView, + typename ActionView, + FixedAngularMomentumPhysicalCore PhysicalCore> + void AddJacobianAction( + const DirectionView &direction, + const ActionView &action, + const PhysicalCore &physical + ) const { + const auto densityDirection = + direction.block(utils::blocks::density_field.mass_term); + const auto surfaceDirection = + direction.block(utils::blocks::surface_deformation_field.parameters_term); + const auto angularVelocityDirection = + direction.block(utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term); + MFEM_VERIFY( + angularVelocityDirection.Size() == 1 && std::isfinite(angularVelocityDirection(0)), + "The angular-velocity direction must be finite." + ); + + m_angularMomentumAction.SetSize(1); + m_angularMomentumAction = 0.0; + physical.GetDomainDeformation().applyJacobian( + physical.GetSurfaceDeformationParameters(), + surfaceDirection, + m_volumeDisplacementDirection + ); + m_constraint.ApplyCompleteJacobianAction( + densityDirection, + m_volumeDisplacementDirection, + angularVelocityDirection(0), + m_angularMomentumAction + ); + action.add( + utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term, + m_angularMomentumAction + ); + + if (m_angularVelocity == 0.0 || angularVelocityDirection(0) == 0.0) { + return; + } + + const double fractionalVariation = angularVelocityDirection(0) / m_angularVelocity; + physical.GetHydrostaticOperator().ApplyRotationAmplitudeJacobianAction( + fractionalVariation, + m_hydrostaticAngularVelocityAction + ); + m_zeroEnthalpy = 0.0; + physical.GetSurfaceConstraintOperator().ApplyJacobianRows( + m_zeroEnthalpy, + m_hydrostaticAngularVelocityAction + ); + action.add( + utils::blocks::enthalpy_field.specific_term, + m_hydrostaticAngularVelocityAction + ); + + physical.GetDisplacementOperator().GetRotationalOperator().BuildResidual( + m_rotationalAngularVelocityAction + ); + m_rotationalAngularVelocityAction *= 2.0 * fractionalVariation; + physical.GetDomainDeformation().applyJacobianTranspose( + physical.GetSurfaceDeformationParameters(), + m_rotationalAngularVelocityAction, + m_surfaceAngularVelocityAction + ); + action.add( + utils::blocks::surface_deformation_field.shape_equilibrium_term, + m_surfaceAngularVelocityAction + ); + } + + [[nodiscard]] bool IsPrepared() const noexcept { + return m_isPrepared && m_constraint.IsPrepared(); + } + + [[nodiscard]] const PreparedAngularMomentumOperator &constraint() const noexcept { + return m_constraint; + } + + private: + PreparedAngularMomentumOperator m_constraint; + StellarEquilibriumDependencyStamp m_generatedRotationDependency; + double m_angularVelocity{0.0}; + bool m_generatedRotationChanged{false}; + bool m_isPrepared{false}; + mutable mfem::Vector m_volumeDisplacementDirection; + mutable mfem::Vector m_rotationalAngularVelocityAction; + mutable mfem::Vector m_surfaceAngularVelocityAction; + mutable mfem::Vector m_hydrostaticAngularVelocityAction; + mutable mfem::Vector m_angularMomentumAction; + mutable mfem::Vector m_zeroEnthalpy; + }; + + struct FixedCentralDensityPreparationReport final { + PreparedCentralDensityReport constraint; + + [[nodiscard]] bool DidAnyWork() const noexcept { + return constraint.DidAnyWork(); + } + }; + + template + class FixedCentralDensityRuntime final { + public: + using Report = FixedCentralDensityPreparationReport; + + template + FixedCentralDensityRuntime( + fem::FEM &finiteElements, + const mapping::DomainMapper &, + PhysicalCore &, + const Model &model + ) + : m_compiled(models::compileConstraint( + model.template specification(), + model.equationOfState() + )), + m_constraint(MakeCenterDofMap(finiteElements), finiteElements.mesh->GetComm()), + m_enthalpyResidual(m_constraint.GetCenterDof().field_size()), + m_phaseResidual(1), + m_enthalpyAction(m_constraint.GetCenterDof().field_size()), + m_phaseAction(1) { + } + + template + void ReadPhysicalControls(const StateView &, StellarEquilibriumControlContext &) noexcept { + } + + template + [[nodiscard]] Report PrepareAfterPhysical( + const StateView &state, + const StellarEquilibriumDependencies &dependencies, + const PhysicalCore & + ) { + const auto enthalpy = state.block(utils::blocks::enthalpy_field.specific_term); + const auto border = + state.block(utils::blocks::fixed_central_density_phase.central_value_term); + MFEM_VERIFY( + border.Size() == 1 && std::isfinite(border(0)), + "The central-density phase border must be finite." + ); + auto report = m_constraint.Prepare( + m_compiled, + enthalpy, + border(0), + {.enthalpy = { + .identity = dependencies.enthalpy.identity, + .revision = dependencies.enthalpy.revision + }} + ); + m_isPrepared = true; + return {.constraint = report}; + } + + template + void AddResidual(const ResidualView &residual) const { + m_enthalpyResidual = 0.0; + m_phaseResidual = 0.0; + m_constraint.AddResidual(m_enthalpyResidual, m_phaseResidual); + residual.add(utils::blocks::enthalpy_field.specific_term, m_enthalpyResidual); + residual.add( + utils::blocks::fixed_central_density_phase.central_value_term, + m_phaseResidual + ); + } + + template < + typename DirectionView, + typename ActionView, + PreparedStellarEquilibriumPhysicalCore PhysicalCore> + void AddJacobianAction( + const DirectionView &direction, + const ActionView &action, + const PhysicalCore & + ) const { + const auto enthalpyDirection = + direction.block(utils::blocks::enthalpy_field.specific_term); + const auto borderDirection = + direction.block(utils::blocks::fixed_central_density_phase.central_value_term); + MFEM_VERIFY( + borderDirection.Size() == 1 && std::isfinite(borderDirection(0)), + "The central-density phase direction must be finite." + ); + m_enthalpyAction = 0.0; + m_phaseAction = 0.0; + m_constraint.ApplyJacobian( + {.enthalpyVariation = enthalpyDirection, .borderVariation = borderDirection(0)}, + {.enthalpyAction = m_enthalpyAction, .phaseAction = m_phaseAction} + ); + action.add(utils::blocks::enthalpy_field.specific_term, m_enthalpyAction); + action.add( + utils::blocks::fixed_central_density_phase.central_value_term, + m_phaseAction + ); + } + + [[nodiscard]] bool IsPrepared() const noexcept { + return m_isPrepared && m_constraint.IsPrepared(); + } + + [[nodiscard]] const PreparedCentralDensityConstraint &constraint() const noexcept { + return m_constraint; + } + + [[nodiscard]] const models::CompiledFixedCentralDensity &compiled() const noexcept { + return m_compiled; + } + + private: + [[nodiscard]] static field::FieldPointDofMap MakeCenterDofMap(const fem::FEM &finiteElements) { + using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; + MFEM_VERIFY( + finiteElements.mesh != nullptr && finiteElements.enthalpyFes != nullptr, + "The central-density phase requires a mesh and enthalpy finite-element space." + ); + const field::FieldDofMap enthalpyMap = + field::make_field_dof_map(*finiteElements.enthalpyFes); + mfem::Vector origin(finiteElements.mesh->SpaceDimension()); + origin = 0.0; + return field::make_field_point_dof_map( + *finiteElements.enthalpyFes, + enthalpyMap, + origin, + 1.0e-12 + ); + } + + models::CompiledFixedCentralDensity m_compiled; + PreparedCentralDensityConstraint m_constraint; + mutable mfem::Vector m_enthalpyResidual; + mutable mfem::Vector m_phaseResidual; + mutable mfem::Vector m_enthalpyAction; + mutable mfem::Vector m_phaseAction; + bool m_isPrepared{false}; + }; + } // namespace detail + + template <> struct StellarEquilibriumRuntimeContribution { + static constexpr bool registered = true; + static constexpr std::size_t rotationProviders = 0; + template + using Prepared = detail::EmbeddedSpecificationRuntime; + }; + + template <> struct StellarEquilibriumRuntimeContribution { + static constexpr bool registered = true; + static constexpr std::size_t rotationProviders = 0; + template + using Prepared = detail::EmbeddedSpecificationRuntime; + }; + + template <> struct StellarEquilibriumRuntimeContribution { + static constexpr bool registered = true; + static constexpr std::size_t rotationProviders = 0; + template + using Prepared = detail::EmbeddedSpecificationRuntime; + }; + + template <> struct StellarEquilibriumRuntimeContribution + : PreparedStellarEquilibriumContribution { }; + + template <> struct StellarEquilibriumRuntimeContribution + : PreparedStellarEquilibriumContribution { }; + + namespace detail { + template + struct BlockListIsSubset : std::false_type { }; + + template + struct BlockListIsSubset, Superset> + : std::bool_constant< + (utils::blocks::contains_type_v && ...)> { }; + + template + struct SinglePhysicsBlock; + + template + struct SinglePhysicsBlock> final { + using Type = Block; + }; + + template + struct PhysicsFacingValueTerm final { + using value = ValueBlock; + }; + + template + struct PhysicsFacingResidualTerm final { + using residual = ResidualBlock; + }; + + template < + typename Form, + typename AllowedValueBlocks, + models::ModelSpecification Specification> + class RestrictedSpecificationStateView final { + public: + explicit RestrictedSpecificationStateView(const RootStateView &state) noexcept + : m_state(state) { + } + + template + requires BlockListIsSubset< + NarrowedValueBlocks, + AllowedValueBlocks>::value + [[nodiscard]] auto narrow() const noexcept { + return RestrictedSpecificationStateView< + Form, + NarrowedValueBlocks, + Specification>{m_state}; + } + + template + requires requires { typename std::remove_cvref_t::value; } && + utils::blocks::contains_type_v< + typename std::remove_cvref_t::value, + AllowedValueBlocks> + [[nodiscard]] auto block(const Term &term) const { + return m_state.block(term); + } + + /* Every compiler-enumerated derivative receives a view containing + * exactly one source. value() is the backend-agnostic spelling + * for advanced physics vocabulary that does not yet have a named + * convenience accessor. */ + [[nodiscard]] auto value() const + requires(AllowedValueBlocks::size == 1) + { + using ValueBlock = typename SinglePhysicsBlock< + AllowedValueBlocks>::Type; + return m_state.block(PhysicsFacingValueTerm{}); + } + + [[nodiscard]] auto density() const + requires StellarDependencyBlock< + Specification, + models::stellar::state::Density>::mapped && + utils::blocks::contains_type_v< + typename StellarDependencyBlock< + Specification, + models::stellar::state::Density>::Type, + AllowedValueBlocks> + { + return physicsBlock(); + } + + [[nodiscard]] auto surfaceShape() const + requires StellarDependencyBlock< + Specification, + models::stellar::state::SurfaceShape>::mapped && + utils::blocks::contains_type_v< + typename StellarDependencyBlock< + Specification, + models::stellar::state::SurfaceShape>::Type, + AllowedValueBlocks> + { + return physicsBlock(); + } + + [[nodiscard]] auto gravityGradient() const + requires StellarDependencyBlock< + Specification, + models::stellar::state::GravityGradient>::mapped && + utils::blocks::contains_type_v< + typename StellarDependencyBlock< + Specification, + models::stellar::state::GravityGradient>::Type, + AllowedValueBlocks> + { + return physicsBlock(); + } + + [[nodiscard]] auto gravitationalPotential() const + requires StellarDependencyBlock< + Specification, + models::stellar::state::GravitationalPotential>::mapped && + utils::blocks::contains_type_v< + typename StellarDependencyBlock< + Specification, + models::stellar::state::GravitationalPotential>::Type, + AllowedValueBlocks> + { + return physicsBlock< + models::stellar::state::GravitationalPotential>(); + } + + [[nodiscard]] auto specificEnthalpy() const + requires StellarDependencyBlock< + Specification, + models::stellar::state::SpecificEnthalpy>::mapped && + utils::blocks::contains_type_v< + typename StellarDependencyBlock< + Specification, + models::stellar::state::SpecificEnthalpy>::Type, + AllowedValueBlocks> + { + return physicsBlock(); + } + + [[nodiscard]] auto generatedCoordinate() const + requires StellarDependencyBlock< + Specification, + models::stellar::state::OwnGeneratedCoordinate>::mapped && + utils::blocks::contains_type_v< + typename StellarDependencyBlock< + Specification, + models::stellar::state::OwnGeneratedCoordinate>::Type, + AllowedValueBlocks> + { + return physicsBlock< + models::stellar::state::OwnGeneratedCoordinate>(); + } + + template + [[nodiscard]] auto generatedCoordinate() const + requires StellarDependencyBlock< + Specification, + models::stellar::state::GeneratedCoordinateOf>::mapped && + utils::blocks::contains_type_v< + typename StellarDependencyBlock< + Specification, + models::stellar::state::GeneratedCoordinateOf>::Type, + AllowedValueBlocks> + { + return physicsBlock< + models::stellar::state::GeneratedCoordinateOf>(); + } + + private: + template + [[nodiscard]] auto physicsBlock() const { + using ValueBlock = typename StellarDependencyBlock< + Specification, + PhysicsQuantity>::Type; + return m_state.block(PhysicsFacingValueTerm{}); + } + + RootStateView m_state; + }; + + template + class RestrictedSpecificationResidualView final { + public: + RestrictedSpecificationResidualView( + const ResidualView &residual, + const PreparedPressureSurfaceConstraint &surfaceConstraint + ) noexcept + : m_residual(residual), + m_surfaceConstraint(std::addressof(surfaceConstraint)) { + } + + template + requires BlockListIsSubset< + AllowedResidualBlocks, + OtherAllowedResidualBlocks>::value + explicit RestrictedSpecificationResidualView( + const RestrictedSpecificationResidualView< + Form, + OtherAllowedResidualBlocks> &residual + ) noexcept + : m_residual(residual.m_residual), + m_surfaceConstraint(residual.m_surfaceConstraint) { + } + + /* Physics-facing assembly is intentionally additive-only. A + * specification cannot erase the physical core or an earlier + * contribution. Hydrostatic additions are also projected away + * from rows owned by the surface condition, so extension authors + * do not need to understand the backend row-replacement policy. */ + template + requires requires { typename std::remove_cvref_t::residual; } && + utils::blocks::contains_type_v< + typename std::remove_cvref_t::residual, + AllowedResidualBlocks> + void add(const Term &term, const double contribution) const { + mfem::Vector destination = m_residual.block(term); + destination += contribution; + RestoreReplacedRows::residual>( + destination, + contribution + ); + destination.SyncAliasMemory(m_residual.vector()); + } + + template + requires requires { typename std::remove_cvref_t::residual; } && + utils::blocks::contains_type_v< + typename std::remove_cvref_t::residual, + AllowedResidualBlocks> + void add(const Term &term, const mfem::Vector &contribution) const { + mfem::Vector destination = m_residual.block(term); + if (destination.Size() != contribution.Size()) { + throw std::invalid_argument( + "A specification runtime contribution has the wrong residual block size." + ); + } + destination += contribution; + RestoreReplacedRows::residual>( + destination, + contribution + ); + destination.SyncAliasMemory(m_residual.vector()); + } + + template + requires requires { typename std::remove_cvref_t::residual; } && + utils::blocks::contains_type_v< + typename std::remove_cvref_t::residual, + AllowedResidualBlocks> + void addEntry( + const Term &term, + const int index, + const double contribution + ) const { + mfem::Vector destination = m_residual.block(term); + if (index < 0 || index >= destination.Size()) { + throw std::out_of_range( + "A specification runtime contribution selected an invalid residual entry." + ); + } + if (!IsReplacedRow::residual>(index)) { + destination(index) += contribution; + } + destination.SyncAliasMemory(m_residual.vector()); + } + + private: + template + friend class RestrictedSpecificationResidualView; + + template + [[nodiscard]] bool IsReplacedRow(const int index) const noexcept { + if constexpr (!std::same_as< + ResidualBlock, + utils::blocks::enthalpy::specific::residual>) { + return false; + } else { + for (const int row : m_surfaceConstraint->GetSurfaceRows().reduced_dofs()) { + if (row == index) { + return true; + } + } + return false; + } + } + + template + void RestoreReplacedRows( + mfem::Vector &destination, + const double contribution + ) const { + if constexpr (std::same_as< + ResidualBlock, + utils::blocks::enthalpy::specific::residual>) { + for (const int row : m_surfaceConstraint->GetSurfaceRows().reduced_dofs()) { + destination(row) -= contribution; + } + } + } + + template + void RestoreReplacedRows( + mfem::Vector &destination, + const mfem::Vector &contribution + ) const { + if constexpr (std::same_as< + ResidualBlock, + utils::blocks::enthalpy::specific::residual>) { + for (const int row : m_surfaceConstraint->GetSurfaceRows().reduced_dofs()) { + destination(row) -= contribution(row); + } + } + } + + ResidualView m_residual; + const PreparedPressureSurfaceConstraint *m_surfaceConstraint; + }; + + /* + * Single-use row handed to one compiler-enumerated physics provider. + * It deliberately has no row selector: the equation tag selected the + * row before the extension was called. A provider therefore cannot + * redirect a legal source into a different legal residual. Runtime + * accounting additionally rejects double assembly or a contribution + * token inconsistent with what the provider actually did. + */ + template + class ExactSpecificationResidualRow final { + public: + using View = RestrictedSpecificationResidualView< + Form, + utils::blocks::type_list>; + + explicit ExactSpecificationResidualRow(const View &row) noexcept + : m_row(row) { + } + + ExactSpecificationResidualRow( + const ExactSpecificationResidualRow & + ) = delete; + ExactSpecificationResidualRow &operator=( + const ExactSpecificationResidualRow & + ) = delete; + ExactSpecificationResidualRow( + ExactSpecificationResidualRow && + ) = delete; + ExactSpecificationResidualRow &operator=( + ExactSpecificationResidualRow && + ) = delete; + + [[nodiscard]] stellar::ContributionAdded add( + const double contribution + ) { + RequireUnused(); + m_row.add( + PhysicsFacingResidualTerm{}, + contribution + ); + m_addCount = 1; + return {}; + } + + [[nodiscard]] stellar::ContributionAdded add( + const mfem::Vector &contribution + ) { + RequireUnused(); + m_row.add( + PhysicsFacingResidualTerm{}, + contribution + ); + m_addCount = 1; + return {}; + } + + [[nodiscard]] stellar::ContributionAdded addEntry( + const int index, + const double contribution + ) { + RequireUnused(); + m_row.addEntry( + PhysicsFacingResidualTerm{}, + index, + contribution + ); + m_addCount = 1; + return {}; + } + + template + void Verify(const Result &) const { + if constexpr (std::same_as< + std::remove_cvref_t, + stellar::ContributionAdded>) { + if (m_addCount != 1) { + throw std::logic_error( + "A stellar physics provider returned ContributionAdded without adding exactly once." + ); + } + } else { + if (m_addCount != 0) { + throw std::logic_error( + "A stellar physics provider returned StructuralZero after adding to its row." + ); + } + } + } + + private: + void RequireUnused() const { + if (m_addCount != 0) { + throw std::logic_error( + "A compiler-enumerated stellar residual/Jacobian edge may be assembled only once." + ); + } + } + + View m_row; + int m_addCount{0}; + }; + + template + struct PhysicsTagList; + + template + struct PhysicsTagList> final { + using Type = utils::blocks::type_list; + }; + + template + struct DerivativesOfEquation; + + template + struct DerivativesOfEquation< + Equation, + utils::blocks::type_list> final { + using Type = utils::blocks::type_list< + models::stellar::Derivative...>; + }; + + template + struct CartesianPhysicsDerivatives; + + template + struct CartesianPhysicsDerivatives< + utils::blocks::type_list, + States> final { + using Type = ConcatenateBlockListsT< + typename DerivativesOfEquation::Type...>; + }; + + /* The topology consumed by physics-facing providers is generated + * directly from the same Reads/Changes declaration used by the block + * compiler. No implementation-owned provider list can get out of + * sync with the model declaration. */ + template + struct SpecificationPhysicsTopology final { + private: + using Contribution = models::SpecificationContribution; + + public: + using ReadStates = typename PhysicsTagList< + typename Contribution::DependsOn>::Type; + using ChangedEquations = typename PhysicsTagList< + typename Contribution::Affects>::Type; + using OwnGeneratedState = std::conditional_t< + Contribution::generatedValueArity == 0, + utils::blocks::type_list<>, + utils::blocks::type_list< + models::stellar::state::OwnGeneratedCoordinate>>; + using OwnConstraintEquation = std::conditional_t< + Contribution::generatedResidualArity == 0, + utils::blocks::type_list<>, + utils::blocks::type_list< + models::stellar::equation::OwnConstraint>>; + + using ResidualEquations = UniqueConcatenateBlockListsT< + OwnConstraintEquation, + ChangedEquations>; + using ChangedEquationInputs = UniqueConcatenateBlockListsT< + ReadStates, + OwnGeneratedState>; + using ConstraintDerivatives = typename CartesianPhysicsDerivatives< + OwnConstraintEquation, + ReadStates>::Type; + using ChangedEquationDerivatives = + typename CartesianPhysicsDerivatives< + ChangedEquations, + ChangedEquationInputs>::Type; + using Derivatives = UniqueConcatenateBlockListsT< + ConstraintDerivatives, + ChangedEquationDerivatives>; + }; + + template < + typename Specification, + typename State, + bool IsSpecification = models::ModelSpecification< + std::remove_cvref_t>> + struct SpecificationReadsState : std::false_type { }; + + template + struct SpecificationReadsState + : std::bool_constant>::ReadStates>> { }; + } // namespace detail + + /** + * Astronomy-facing access to the current core's physical-volume density + * integral, + * + * M[rho] = integral_{Omega_star} rho dV. + * + * The context is available only to a specification which declares both + * stellar::state::Density and stellar::state::SurfaceShape in Reads. That + * is a mathematical requirement rather than an implementation detail: + * the mapped physical-volume integral depends on both rho and the domain + * geometry. Requiring both declarations prevents a residual from using + * this service while omitting its geometry column from the inferred + * Jacobian. The context is a small, copyable, non-owning service handle: + * it exposes neither FEM objects nor the physical core, owns no backend + * object, and uses no allocating type erasure. The selected core remains + * responsible for quadrature, mapped physical volume, distributed + * reduction, scratch storage, and exact directional actions. + */ + template + concept DensityVolumeIntegralSpecification = + detail::SpecificationReadsState< + Specification, + models::stellar::state::Density>::value && + detail::SpecificationReadsState< + Specification, + models::stellar::state::SurfaceShape>::value; + + template + class DensityVolumeIntegralContext final { + public: + [[nodiscard]] dimensions::MassValue integrateDensity( + const mfem::Vector &density + ) const { + return dimensions::MassValue{m_densityAction(m_core, density)}; + } + + [[nodiscard]] dimensions::MassValue linearizeDensityIntegral( + const mfem::Vector &densityDirection + ) const { + return dimensions::MassValue{ + m_densityAction(m_core, densityDirection)}; + } + + [[nodiscard]] dimensions::MassValue linearizeSurfaceShapeIntegral( + const mfem::Vector &surfaceShapeDirection + ) const { + return dimensions::MassValue{ + m_surfaceShapeAction(m_core, surfaceShapeDirection)}; + } + + private: + template < + models::ModelSpecification OtherSpecification, + model::StellarModelType OtherModel, + typename OtherPhysics> + friend class detail::PhysicsFacingSpecificationRuntime; + + using Action = double (*)(const void *, const mfem::Vector &); + + public: + /* Public construction is intentionally backend-facing: it accepts a + * core which already owns the prepared integration service, but the + * resulting physics-facing handle has no route back to that core. This + * also permits direct distributed contract tests of the service. */ + template + requires requires( + const PhysicalCore &core, + const mfem::Vector &direction + ) { + { + core.ApplyDensityVolumeIntegralDensityAction(direction) + } -> std::convertible_to; + { + core.ApplyDensityVolumeIntegralSurfaceShapeAction(direction) + } -> std::convertible_to; + } + explicit DensityVolumeIntegralContext( + const PhysicalCore &core + ) noexcept + : m_core(std::addressof(core)), + m_densityAction(&ApplyDensityAction), + m_surfaceShapeAction(&ApplySurfaceShapeAction) { + } + + private: + template + [[nodiscard]] static double ApplyDensityAction( + const void *untypedCore, + const mfem::Vector &densityDirection + ) { + const auto &core = *static_cast(untypedCore); + return core.ApplyDensityVolumeIntegralDensityAction( + densityDirection + ); + } + + template + [[nodiscard]] static double ApplySurfaceShapeAction( + const void *untypedCore, + const mfem::Vector &surfaceShapeDirection + ) { + const auto &core = *static_cast(untypedCore); + return core.ApplyDensityVolumeIntegralSurfaceShapeAction( + surfaceShapeDirection + ); + } + + const void *m_core{nullptr}; + Action m_densityAction{nullptr}; + Action m_surfaceShapeAction{nullptr}; + }; + + namespace detail { + template < + models::ModelSpecification Specification, + typename Physics, + bool ContextAvailable = + DensityVolumeIntegralSpecification> + struct DensityVolumeIntegralPhysicsConstruction final { + using Context = void; + static constexpr bool constructible = false; + }; + + template + struct DensityVolumeIntegralPhysicsConstruction< + Specification, + Physics, + true> final { + using Context = DensityVolumeIntegralContext; + static constexpr bool constructible = std::constructible_from< + Physics, + const Specification &, + Context>; + }; + + template + concept DensityVolumeIntegralCore = requires( + const PhysicalCore &core, + const mfem::Vector &direction + ) { + { + core.ApplyDensityVolumeIntegralDensityAction(direction) + } -> std::convertible_to; + { + core.ApplyDensityVolumeIntegralSurfaceShapeAction(direction) + } -> std::convertible_to; + }; + + /* + * A Jacobian callback is stricter than residual assembly. Its + * row/source pair is checked against the compiler output, then the + * callback receives a direction view containing only that source and + * an additive action view containing only that row. Consequently, + * independently legal endpoints cannot accidentally be recombined + * into an undeclared edge inside one callback. As with any assembly + * API, this structural contract does not attempt to prove that the + * callback's arithmetic is the mathematical derivative it claims. + */ + template < + typename Form, + typename AllowedCouplings, + typename AllowedValueBlocks, + models::ModelSpecification Specification, + typename AllowedResidualBlocks> + class RestrictedSpecificationJacobianView final { + public: + RestrictedSpecificationJacobianView( + const RestrictedSpecificationStateView< + Form, + AllowedValueBlocks, + Specification> &direction, + const RestrictedSpecificationResidualView< + Form, + AllowedResidualBlocks> &action + ) noexcept + : m_direction(direction), + m_action(action) { + } + + template + requires requires { + typename std::remove_cvref_t::residual; + typename std::remove_cvref_t::value; + } && + utils::blocks::contains_type_v< + typename std::remove_cvref_t::value, + AllowedValueBlocks> && + utils::blocks::contains_type_v< + typename std::remove_cvref_t::residual, + AllowedResidualBlocks> && + utils::blocks::contains_type_v< + StellarEquilibriumJacobianCoupling< + typename std::remove_cvref_t::residual, + typename std::remove_cvref_t::value>, + AllowedCouplings> && + requires( + Callback &&callback, + RestrictedSpecificationStateView< + Form, + utils::blocks::type_list< + typename std::remove_cvref_t::value>, + Specification> &direction, + RestrictedSpecificationResidualView< + Form, + utils::blocks::type_list< + typename std::remove_cvref_t::residual>> &action + ) { + { + std::forward(callback)(direction, action) + } -> std::same_as; + } + void add( + const ResidualTerm &, + const ValueTerm &, + Callback &&callback + ) const { + using DirectionView = RestrictedSpecificationStateView< + Form, + utils::blocks::type_list< + typename std::remove_cvref_t::value>, + Specification>; + using ActionView = RestrictedSpecificationResidualView< + Form, + utils::blocks::type_list< + typename std::remove_cvref_t::residual>>; + + DirectionView direction = m_direction.template narrow< + utils::blocks::type_list< + typename std::remove_cvref_t::value>>(); + ActionView action{m_action}; + std::forward(callback)(direction, action); + } + + private: + RestrictedSpecificationStateView< + Form, + AllowedValueBlocks, + Specification> m_direction; + RestrictedSpecificationResidualView m_action; + }; + + template + struct CompilePhysicsTags; + + template + struct CompilePhysicsTags< + Specification, + utils::blocks::type_list> final { + using Type = utils::blocks::type_list< + typename StellarDependencyBlock::Type...>; + static constexpr bool complete = + (StellarDependencyBlock::mapped && ...); + }; + + template + struct CompilePhysicsDerivatives; + + template + struct CompilePhysicsDerivative; + + template < + models::ModelSpecification Specification, + typename Equation, + typename State> + struct CompilePhysicsDerivative< + Specification, + models::stellar::Derivative> final { + using Type = StellarEquilibriumJacobianCoupling< + typename StellarDependencyBlock< + Specification, + Equation>::Type, + typename StellarDependencyBlock< + Specification, + State>::Type>; + static constexpr bool complete = + StellarDependencyBlock::mapped && + StellarDependencyBlock::mapped; + }; + + template + struct CompilePhysicsDerivatives< + Specification, + utils::blocks::type_list> final { + using Type = utils::blocks::type_list< + typename CompilePhysicsDerivative< + Specification, + Derivatives>::Type...>; + static constexpr bool complete = + (CompilePhysicsDerivative< + Specification, + Derivatives>::complete && ...); + }; + + template + struct SpecificationRuntimeAccess final { + using SpecificationType = Specification; + using Compilation = StellarEquilibriumSpecificationCompilation; + using Topology = SpecificationPhysicsTopology; + using Form = CompiledStellarEquilibriumForm; + using ValueBlocks = UniqueConcatenateBlockListsT< + typename Compilation::GeneratedValueBlocks, + typename Compilation::DependsOnValueBlocks>; + using ResidualBlocks = UniqueConcatenateBlockListsT< + typename Compilation::GeneratedResidualBlocks, + typename Compilation::AffectedResidualBlocks>; + using StateView = RestrictedSpecificationStateView< + Form, + ValueBlocks, + Specification>; + using ResidualView = RestrictedSpecificationResidualView; + + template + using ValueBlockFor = typename StellarDependencyBlock< + Specification, + StateTag>::Type; + + template + using ResidualBlockFor = typename StellarDependencyBlock< + Specification, + EquationTag>::Type; + + template + using DirectionView = RestrictedSpecificationStateView< + Form, + utils::blocks::type_list>, + Specification>; + + template + using RowView = RestrictedSpecificationResidualView< + Form, + utils::blocks::type_list>>; + + template + using Row = ExactSpecificationResidualRow< + Form, + ResidualBlockFor>; + + using JacobianView = RestrictedSpecificationJacobianView< + Form, + typename Compilation::JacobianCouplings, + ValueBlocks, + Specification, + ResidualBlocks>; + + using ProviderResidualBlocks = typename CompilePhysicsTags< + Specification, + typename Topology::ResidualEquations>::Type; + using ProviderJacobianCouplings = typename CompilePhysicsDerivatives< + Specification, + typename Topology::Derivatives>::Type; + + static_assert(CompilePhysicsTags< + Specification, + typename Topology::ResidualEquations>::complete); + static_assert(CompilePhysicsDerivatives< + Specification, + typename Topology::Derivatives>::complete); + static_assert(std::same_as< + ProviderResidualBlocks, + ResidualBlocks>); + static_assert(std::same_as< + ProviderJacobianCouplings, + typename Compilation::JacobianCouplings>); + }; + + template + struct ExactResidualProviderSet; + + template + struct ExactResidualProviderSet< + Physics, + Access, + utils::blocks::type_list> final { + static constexpr bool complete = + (requires( + const Physics &physics, + typename Access::template Row &row + ) { + { + physics.AddResidual(Equations{}, row) + } -> stellar::ContributionResult; + } && ...); + + static void Apply( + const Physics &physics, + const typename Access::ResidualView &residual + ) requires complete { + (ApplyOne(physics, residual), ...); + } + + private: + template + static void ApplyOne( + const Physics &physics, + const typename Access::ResidualView &residual + ) { + typename Access::template RowView rowView{residual}; + typename Access::template Row row{rowView}; + decltype(auto) result = physics.AddResidual( + Equation{}, + row + ); + row.Verify(result); + } + }; + + template + struct ExactJacobianProviderSet; + + template + struct ExactJacobianProviderSet< + Physics, + Access, + utils::blocks::type_list> final { + private: + template + struct Traits; + + template + struct Traits> final { + using EquationTag = Equation; + using StateTag = State; + }; + + template + [[nodiscard]] static consteval bool ProviderIsComplete() { + using Equation = typename Traits::EquationTag; + using State = typename Traits::StateTag; + return requires( + const Physics &physics, + const typename Access::template DirectionView &direction, + typename Access::template Row &row + ) { + { + physics.AddJacobianAction( + Derivative{}, + direction, + row + ) + } -> stellar::ContributionResult; + }; + } + + public: + static constexpr bool complete = + (ProviderIsComplete() && ...); + + static void Apply( + const Physics &physics, + const typename Access::StateView &direction, + const typename Access::ResidualView &action + ) requires complete { + (ApplyOne(physics, direction, action), ...); + } + + private: + template + static void ApplyOne( + const Physics &physics, + const typename Access::StateView &direction, + const typename Access::ResidualView &action + ) { + using Equation = typename Traits::EquationTag; + using State = typename Traits::StateTag; + typename Access::template DirectionView source = + direction.template narrow>>(); + typename Access::template RowView rowView{action}; + typename Access::template Row row{rowView}; + decltype(auto) result = physics.AddJacobianAction( + Derivative{}, + source, + row + ); + row.Verify(result); + } + }; + + template + inline constexpr bool exactSpecificationPhysicsProvidersComplete = + ExactResidualProviderSet< + Physics, + Access, + typename Access::Topology::ResidualEquations>::complete && + ExactJacobianProviderSet< + Physics, + Access, + typename Access::Topology::Derivatives>::complete; + + /* Classify one exact nested derivative provider by its return type. + * This information is useful outside residual assembly as well: a + * preconditioner may omit a compiler-declared core-to-core edge only + * when the physics implementation itself proves that the edge is the + * identically zero map. The primary remains well formed so capability + * queries for malformed providers fail normally rather than producing + * diagnostics deep in a factory body. */ + template < + typename Physics, + typename Access, + typename Derivative, + typename = void> + struct ExactJacobianProviderResult final { + using Coupling = void; + using Result = void; + + static constexpr bool complete = false; + static constexpr bool structuralZero = false; + }; + + template < + typename Physics, + typename Access, + typename Equation, + typename State> + struct ExactJacobianProviderResult< + Physics, + Access, + models::stellar::Derivative, + std::void_t().AddJacobianAction( + models::stellar::Derivative{}, + std::declval &>(), + std::declval &>() + ))>> final { + using Derivative = models::stellar::Derivative; + using Coupling = typename CompilePhysicsDerivative< + typename Access::SpecificationType, + Derivative>::Type; + using Result = decltype(std::declval().AddJacobianAction( + Derivative{}, + std::declval &>(), + std::declval &>() + )); + + static constexpr bool complete = + stellar::ContributionResult; + static constexpr bool structuralZero = + complete && std::same_as< + std::remove_cvref_t, + stellar::StructuralZero>; + }; + + template < + typename Physics, + typename Access, + typename Coupling, + typename Derivatives> + struct ExactCouplingProvidersAreStructuralZero; + + template < + typename Physics, + typename Access, + typename Coupling, + typename... Derivatives> + struct ExactCouplingProvidersAreStructuralZero< + Physics, + Access, + Coupling, + utils::blocks::type_list> final { + private: + template + using Provider = ExactJacobianProviderResult< + Physics, + Access, + Derivative>; + + static constexpr bool hasMatchingProvider = + (false || ... || std::same_as< + Coupling, + typename Provider::Coupling>); + static constexpr bool everyMatchingProviderIsZero = + (true && ... && + (!std::same_as< + Coupling, + typename Provider::Coupling> || + Provider::structuralZero)); + + public: + static constexpr bool value = + hasMatchingProvider && everyMatchingProviderIsZero; + }; + + template < + typename Specification, + typename Model, + typename Coupling, + typename = void> + struct NestedSpecificationCouplingIsStructuralZero + : std::false_type { }; + + template < + models::ModelSpecification Specification, + model::StellarModelType Model, + typename Coupling> + requires Model::template containsSpecification + struct NestedSpecificationCouplingIsStructuralZero< + Specification, + Model, + Coupling, + std::void_t< + typename RuntimeContributionSelection< + Specification, + Model>::Physics>> final + : std::bool_constant< + RuntimeContributionSelection::available && + RuntimeContributionSelection::registered && + !RuntimeContributionSelection::ambiguous && + ExactCouplingProvidersAreStructuralZero< + typename RuntimeContributionSelection< + Specification, + Model>::Physics, + SpecificationRuntimeAccess, + Coupling, + typename SpecificationPhysicsTopology< + Specification>::Derivatives>::value> { }; + + /* + * Adapter for the physics-facing nested extension protocol. + * + * The outer runtime constructs this object through the same internal + * slot interface as trusted backend contributions. The authored + * Physics object behind it sees a deliberately smaller interface: + * + * Physics(const Specification &) + * // or, only with Reads and a supporting core: + * Physics(const Specification &, DensityVolumeIntegralContext) + * Report PrepareAfterPhysical(const StateView &) + * ContributionResult AddResidual(EquationTag, Row &) const + * ContributionResult AddJacobianAction( + * stellar::Derivative, + * const one-source DirectionView &, + * Row &) const + * + * The adapter invokes those overloads once for every row/edge inferred + * from Reads/Changes. row.add(...) returns the required success token; + * an identically absent term must return stellar::structuralZero. + * Missing overloads fail the capability query at compile time, while + * double assembly and inconsistent result tokens fail immediately at + * runtime. Physics authors never enumerate a backend provider list or + * see an aggregate direction/action object. + * + * bool IsPrepared() const + * + * A nested contribution which owns rotation additionally supplies + * `RigidRotation GenerateRotation(const StateView &)`. The adapter, + * rather than the extension, owns the dependency stamp and compares + * successive rotations. In particular, no nested implementation is + * ever handed the model, FEM/mapper objects, dependency set, control + * context, or physical core. The optional integral context is a + * read-only service handle and does not expose any of those objects. + */ + template < + models::ModelSpecification Specification, + model::StellarModelType Model, + typename Physics> + class PhysicsFacingSpecificationRuntime final { + private: + using Access = SpecificationRuntimeAccess; + using IntegralConstruction = + DensityVolumeIntegralPhysicsConstruction; + using ResidualProviders = ExactResidualProviderSet< + Physics, + Access, + typename Access::Topology::ResidualEquations>; + using JacobianProviders = ExactJacobianProviderSet< + Physics, + Access, + typename Access::Topology::Derivatives>; + static constexpr std::size_t rotationProviders = + RuntimeContributionSelection::rotationProviders; + + public: + using Report = typename Physics::Report; + + template + requires( + IntegralConstruction::constructible && + DensityVolumeIntegralCore) + PhysicsFacingSpecificationRuntime( + fem::FEM &, + const mapping::DomainMapper &, + PhysicalCore &physical, + const Model &model + ) + : m_physics( + model.template specification(), + typename IntegralConstruction::Context{physical}) { + m_generatedRotationDependency.identity = + static_cast(reinterpret_cast(this)); + } + + template + requires( + std::constructible_from && + (!IntegralConstruction::constructible || + !DensityVolumeIntegralCore)) + PhysicsFacingSpecificationRuntime( + fem::FEM &, + const mapping::DomainMapper &, + PhysicalCore &, + const Model &model + ) + : m_physics(model.template specification()) { + m_generatedRotationDependency.identity = + static_cast(reinterpret_cast(this)); + } + + template + requires( + rotationProviders == 0 || + requires(Physics &implementation, const StateView &state) { + { + implementation.GenerateRotation(state) + } -> std::same_as; + }) + void ReadPhysicalControls( + const StateView &state, + StellarEquilibriumControlContext &context + ) { + if constexpr (rotationProviders == 1) { + physics::RigidRotation rotation = m_physics.GenerateRotation(state); + const bool changed = !m_generatedRotation.has_value() || + !SameRotation(*m_generatedRotation, rotation); + if (changed) { + m_generatedRotation = rotation; + ++m_generatedRotationDependency.revision; + } + context.dependencies.rotation = m_generatedRotationDependency; + context.rotation = std::move(rotation); + ++context.rotationProviderCount; + context.generatedPhysicalControl = + context.generatedPhysicalControl || changed; + } + } + + template + requires requires(Physics &implementation, const StateView &state) { + { + implementation.PrepareAfterPhysical(state) + } -> std::same_as; + } + [[nodiscard]] Report PrepareAfterPhysical( + const StateView &state, + const StellarEquilibriumDependencies &, + const PhysicalCore & + ) { + return m_physics.PrepareAfterPhysical(state); + } + + void AddResidual(const typename Access::ResidualView &residual) const + requires ResidualProviders::complete + { + ResidualProviders::Apply(m_physics, residual); + } + + template + requires JacobianProviders::complete + void AddJacobianAction( + const typename Access::StateView &direction, + const typename Access::ResidualView &action, + const PhysicalCore & + ) const { + JacobianProviders::Apply(m_physics, direction, action); + } + + [[nodiscard]] bool IsPrepared() const noexcept + requires requires(const Physics &implementation) { + { + implementation.IsPrepared() + } -> std::convertible_to; + } + { + return static_cast(m_physics.IsPrepared()); + } + + [[nodiscard]] Physics &physics() noexcept { + return m_physics; + } + + [[nodiscard]] const Physics &physics() const noexcept { + return m_physics; + } + + private: + [[nodiscard]] static bool SameVector( + const mfem::Vector &left, + const mfem::Vector &right + ) noexcept { + if (left.Size() != right.Size()) { + return false; + } + for (int component = 0; component < left.Size(); ++component) { + if (left(component) != right(component)) { + return false; + } + } + return true; + } + + [[nodiscard]] static bool SameRotation( + const physics::RigidRotation &left, + const physics::RigidRotation &right + ) noexcept { + return SameVector(left.angular_velocity(), right.angular_velocity()) && + SameVector(left.center(), right.center()); + } + + Physics m_physics; + std::optional m_generatedRotation; + StellarEquilibriumDependencyStamp m_generatedRotationDependency{}; + }; + + template < + model::StellarModelType Model, + PreparedStellarEquilibriumPhysicalCore PhysicalCore, + typename SpecificationSet> + class PreparedSpecificationSet; + + template + class PreparedSpecificationSet< + Model, + PhysicalCore, + models::detail::SpecificationSetStorage<>> final { + public: + PreparedSpecificationSet( + fem::FEM &, + const mapping::DomainMapper &, + PhysicalCore &, + const Model & + ) noexcept { + } + + template + void ReadPhysicalControls(const StateView &, StellarEquilibriumControlContext &) noexcept { + } + + template + void PrepareAfterPhysical( + const StateView &, + const StellarEquilibriumDependencies &, + const PhysicalCore &, + Reports & + ) noexcept { + } + + template + void AddResidual(const ResidualView &, const PhysicalCore &) const noexcept { + } + + template + void AddJacobianAction( + const DirectionView &, + const ActionView &, + const PhysicalCore & + ) const noexcept { + } + + [[nodiscard]] constexpr bool IsPrepared() const noexcept { + return true; + } + }; + + template < + model::StellarModelType Model, + PreparedStellarEquilibriumPhysicalCore PhysicalCore, + models::ModelSpecification Head, + models::ModelSpecification... Tail> + class PreparedSpecificationSet< + Model, + PhysicalCore, + models::detail::SpecificationSetStorage> final { + private: + using HeadSlot = PreparedRuntimeContribution; + using HeadAccess = SpecificationRuntimeAccess; + using TailSlots = PreparedSpecificationSet< + Model, + PhysicalCore, + models::detail::SpecificationSetStorage>; + + public: + PreparedSpecificationSet( + fem::FEM &finiteElements, + const mapping::DomainMapper &domainMapper, + PhysicalCore &physical, + const Model &model + ) + : m_head(finiteElements, domainMapper, physical, model), + m_tail(finiteElements, domainMapper, physical, model) { + } + + template + void ReadPhysicalControls( + const StateView &state, + StellarEquilibriumControlContext &context + ) { + m_head.ReadPhysicalControls(typename HeadAccess::StateView{state}, context); + m_tail.ReadPhysicalControls(state, context); + } + + template + void PrepareAfterPhysical( + const StateView &state, + const StellarEquilibriumDependencies &dependencies, + const PhysicalCore &physical, + Reports &reports + ) { + std::get(reports) = m_head.PrepareAfterPhysical( + typename HeadAccess::StateView{state}, dependencies, physical + ); + m_tail.template PrepareAfterPhysical(state, dependencies, physical, reports); + } + + template + void AddResidual( + const ResidualView &residual, + const PhysicalCore &physical + ) const { + m_head.AddResidual(typename HeadAccess::ResidualView{ + residual, + physical.GetSurfaceConstraintOperator() + }); + m_tail.AddResidual(residual, physical); + } + + template + void AddJacobianAction( + const DirectionView &direction, + const ActionView &action, + const PhysicalCore &physical + ) const { + m_head.AddJacobianAction( + typename HeadAccess::StateView{direction}, + typename HeadAccess::ResidualView{ + action, + physical.GetSurfaceConstraintOperator() + }, + physical + ); + m_tail.AddJacobianAction(direction, action, physical); + } + + [[nodiscard]] bool IsPrepared() const noexcept { + return m_head.IsPrepared() && m_tail.IsPrepared(); + } + + template + [[nodiscard]] auto &Get() noexcept { + if constexpr (std::same_as) { + if constexpr (HasNestedStellarEquilibriumPhysics) { + return m_head.physics(); + } else { + return m_head; + } + } else { + return m_tail.template Get(); + } + } + + template + [[nodiscard]] const auto &Get() const noexcept { + if constexpr (std::same_as) { + if constexpr (HasNestedStellarEquilibriumPhysics) { + return m_head.physics(); + } else { + return m_head; + } + } else { + return m_tail.template Get(); + } + } + + private: + HeadSlot m_head; + TailSlots m_tail; + }; + + template < + models::ModelSpecification Specification, + model::StellarModelType Model, + bool SymbolicallyCompilable = + StellarEquilibriumSystemCompilable && + CoreRuntimeInterfaceAudit>::complete, + typename = void> + struct RuntimeContributionInterfaceAudit { + static constexpr bool complete = false; + static constexpr std::size_t rotationProviders = 0; + }; + + template + struct RuntimeContributionInterfaceAudit< + Specification, + Model, + true, + std::void_t< + PreparedRuntimeContribution, + typename PreparedRuntimeContribution::Report, + std::bool_constant( + RuntimeContributionSelection::registered)>, + std::integral_constant< + std::size_t, + static_cast( + RuntimeContributionSelection::rotationProviders)>>> { + private: + using Selection = RuntimeContributionSelection; + using Prepared = PreparedRuntimeContribution; + using PhysicalCore = typename CoreRuntimeInterfaceAudit::CoreType; + using Access = SpecificationRuntimeAccess; + using StateView = typename Access::StateView; + using ResidualViewType = typename Access::ResidualView; + + public: + using Report = typename Prepared::Report; + + static constexpr bool complete = requires( + fem::FEM &finiteElements, + const mapping::DomainMapper &domainMapper, + PhysicalCore &physical, + const PhysicalCore &constantPhysical, + const Model &model, + Prepared &prepared, + const Prepared &constantPrepared, + const StateView &state, + const ResidualViewType &residual, + StellarEquilibriumControlContext &controls, + const StellarEquilibriumDependencies &dependencies + ) { + requires Selection::registered; + requires !Selection::ambiguous; + requires std::default_initializable; + requires std::assignable_from; + requires std::constructible_from< + Prepared, + fem::FEM &, + const mapping::DomainMapper &, + PhysicalCore &, + const Model &>; + { + prepared.ReadPhysicalControls(state, controls) + } -> std::same_as; + { + prepared.PrepareAfterPhysical(state, dependencies, constantPhysical) + } -> std::same_as; + { + constantPrepared.AddResidual(residual) + } -> std::same_as; + { + constantPrepared.AddJacobianAction(state, residual, constantPhysical) + } -> std::same_as; + { + constantPrepared.IsPrepared() + } -> std::convertible_to; + }; + static constexpr std::size_t rotationProviders = + complete ? Selection::rotationProviders : 0; + }; + + template + struct RuntimeContributionAuditImpl; + + template + struct RuntimeContributionAuditImpl< + Model, + models::detail::SpecificationSetStorage, + false> { + static constexpr bool complete = false; + static constexpr std::size_t rotationProviders = 0; + using ReportTuple = std::tuple<>; + }; + + template + struct RuntimeContributionAuditImpl< + Model, + models::detail::SpecificationSetStorage, + true> { + static constexpr bool complete = true; + static constexpr std::size_t rotationProviders = + (std::size_t{0} + ... + RuntimeContributionInterfaceAudit::rotationProviders); + using ReportTuple = std::tuple< + typename RuntimeContributionInterfaceAudit::Report...>; + }; + + template + struct RuntimeContributionAudit; + + template + struct RuntimeContributionAudit> + : RuntimeContributionAuditImpl< + Model, + models::detail::SpecificationSetStorage, + (RuntimeContributionInterfaceAudit::complete && ...)> { }; + + template + struct MakeValueSizes; + + template + struct MakeValueSizes> { + template + [[nodiscard]] static std::array Apply( + const utils::blocks::form_layout &physicalLayout + ) { + return {BlockSize(physicalLayout)...}; + } + + private: + template + [[nodiscard]] static int BlockSize(const utils::blocks::form_layout &physicalLayout) { + if constexpr (utils::blocks::contains_type_v) { + constexpr int index = utils::blocks::type_index_v; + return physicalLayout.value_offsets()[index + 1] - physicalLayout.value_offsets()[index]; + } else { + static_assert( + Block::static_block_size != utils::blocks::dynamic_block_size, + "A generated stellar-equilibrium value block must have a compile-time size." + ); + return Block::static_block_size; + } + } + }; + + template + struct MakeResidualSizes; + + template + struct MakeResidualSizes> { + template + [[nodiscard]] static std::array Apply( + const utils::blocks::form_layout &physicalLayout + ) { + return {BlockSize(physicalLayout)...}; + } + + private: + template + [[nodiscard]] static int BlockSize(const utils::blocks::form_layout &physicalLayout) { + if constexpr (utils::blocks::contains_type_v) { + constexpr int index = utils::blocks::type_index_v; + return physicalLayout.residual_offsets()[index + 1] - physicalLayout.residual_offsets()[index]; + } else { + static_assert( + Block::static_block_size != utils::blocks::dynamic_block_size, + "A generated stellar-equilibrium residual block must have a compile-time size." + ); + return Block::static_block_size; + } + } + }; + + template + struct AllBlocksBelongToList : std::false_type { }; + + template + struct AllBlocksBelongToList< + utils::blocks::type_list, + AvailableBlocks> + : std::bool_constant< + (utils::blocks::contains_type_v && ...)> { }; + + template < + model::StellarModelType Model, + bool HasCompiledPhysicalRoot = + StellarEquilibriumSystemCompilable && + std::remove_cvref_t::template containsSpecification> + struct PhysicalRootCompatibilityAudit { + static constexpr bool complete = false; + }; + + template + struct PhysicalRootCompatibilityAudit { + private: + using ModelType = std::remove_cvref_t; + using RootForm = CompiledStellarEquilibriumForm; + using PhysicalForm = utils::blocks::surface_deformed_stellar_equilibrium_form; + + public: + static constexpr bool complete = + AllBlocksBelongToList< + typename PhysicalForm::value_blocks, + typename RootForm::value_blocks>::value && + AllBlocksBelongToList< + typename PhysicalForm::residual_blocks, + typename RootForm::residual_blocks>::value; + }; + + template + struct SpecificationIndex; + + template + struct SpecificationIndex< + Specification, + models::detail::SpecificationSetStorage> + : std::integral_constant { }; + + template + struct SpecificationIndex< + Specification, + models::detail::SpecificationSetStorage> + : std::integral_constant< + std::size_t, + 1 + SpecificationIndex< + Specification, + models::detail::SpecificationSetStorage>::value> { }; + + template + struct SpecificationIndex>; + } // namespace detail + + /* + * Physics-extension views expose exactly the blocks declared by one + * specification. Generated coordinates/rows are included automatically; + * no extension author needs to spell out backend block lists twice. + */ + template + concept StellarEquilibriumSpecificationBelongsToModel = + models::ModelSpecification> && + model::StellarModelType> && requires { + requires std::remove_cvref_t::template containsSpecification< + std::remove_cvref_t>; + }; + + template + requires StellarEquilibriumSpecificationBelongsToModel + using StellarEquilibriumContributionStateView = + typename detail::SpecificationRuntimeAccess< + std::remove_cvref_t, + std::remove_cvref_t>::StateView; + + template + requires StellarEquilibriumSpecificationBelongsToModel + using StellarEquilibriumContributionResidualView = + typename detail::SpecificationRuntimeAccess< + std::remove_cvref_t, + std::remove_cvref_t>::ResidualView; + + /* Low-level topology-inspection view retained for compiler tests and + * backend adapters. Physics-facing nested runtimes are not handed this + * imperative object; they use the exact provider protocol below. */ + template + requires StellarEquilibriumSpecificationBelongsToModel + using StellarEquilibriumContributionJacobianView = + typename detail::SpecificationRuntimeAccess< + std::remove_cvref_t, + std::remove_cvref_t>::JacobianView; + + template + requires StellarEquilibriumSpecificationBelongsToModel + using StellarEquilibriumContributionTopology = + typename detail::SpecificationRuntimeAccess< + std::remove_cvref_t, + std::remove_cvref_t>::Topology; + + template + requires StellarEquilibriumSpecificationBelongsToModel + using StellarEquilibriumContributionDirection = + typename detail::SpecificationRuntimeAccess< + std::remove_cvref_t, + std::remove_cvref_t>::template DirectionView; + + template + requires StellarEquilibriumSpecificationBelongsToModel + using StellarEquilibriumContributionRow = + typename detail::SpecificationRuntimeAccess< + std::remove_cvref_t, + std::remove_cvref_t>::template Row; + + /* A focused diagnostic concept for extension authors. It answers the + * useful question directly: does this prepared physics class implement + * every residual row and derivative inferred from my declaration? */ + template + concept CompleteStellarEquilibriumPhysicsProvider = + StellarEquilibriumSpecificationBelongsToModel && + detail::exactSpecificationPhysicsProvidersComplete< + std::remove_cvref_t, + detail::SpecificationRuntimeAccess< + std::remove_cvref_t, + std::remove_cvref_t>>; + + /* True only when the exact nested physics provider corresponding to this + * compiled coupling returns StructuralZero. This is intentionally a + * proof about the provider's type, not a second author-written metadata + * flag. Preconditioners use it to distinguish an intentional zero from a + * nonzero contribution that their selected structure backend must either + * implement or reject. */ + template + inline constexpr bool + stellarEquilibriumSpecificationCouplingIsStructuralZero = + StellarEquilibriumSpecificationBelongsToModel< + Specification, + Model> && + detail::NestedSpecificationCouplingIsStructuralZero< + std::remove_cvref_t, + std::remove_cvref_t, + std::remove_cvref_t>::value; + + template + concept StellarEquilibriumPhysicsAvailableFor = + StellarEquilibriumSpecificationBelongsToModel && + detail::RuntimeContributionInterfaceAudit< + std::remove_cvref_t, + std::remove_cvref_t>::complete; + + template + inline constexpr bool hasCompleteStellarEquilibriumRuntime = + detail::RuntimeContributionAudit< + std::remove_cvref_t, + typename std::remove_cvref_t::SpecificationTypes>::complete; + + template + inline constexpr bool hasStellarEquilibriumCoreRuntime = + detail::CoreRuntimeInterfaceAudit>::complete; + + template + requires hasStellarEquilibriumCoreRuntime + using StellarEquilibriumPhysicalCoreType = + typename detail::CoreRuntimeInterfaceAudit>::CoreType; + + template + inline constexpr std::size_t stellarEquilibriumRotationProviderCount = + detail::RuntimeContributionAudit< + std::remove_cvref_t, + typename std::remove_cvref_t::SpecificationTypes>::rotationProviders; + + template + inline constexpr bool hasCompatibleStellarEquilibriumPhysicalRoot = + detail::PhysicalRootCompatibilityAudit>::complete; + + template + requires hasCompleteStellarEquilibriumRuntime + struct PreparedVariadicStellarEquilibriumReport final { + using ModelType = std::remove_cvref_t; + using SpecificationTypes = typename ModelType::SpecificationTypes; + using SpecificationReports = + typename detail::RuntimeContributionAudit::ReportTuple; + + PreparedStellarEquilibriumReport physical; + SpecificationReports specifications; + bool generatedPhysicalControl{false}; + bool assembledResidual{false}; + + template + requires ModelType::template containsSpecification + [[nodiscard]] const auto &specification() const noexcept { + constexpr std::size_t index = detail::SpecificationIndex::value; + return std::get(specifications); + } + + [[nodiscard]] bool DidAnyWork() const noexcept { + return physical.DidAnyWork() || generatedPhysicalControl || assembledResidual; + } + }; + + /** + * One runtime root for every fully supported specification pack. + * + * The class template itself is the inferred type. Its slot set is a + * recursive, statically dispatched fold, so adding a specification never + * creates a new hand-written combination class or a runtime registry. + */ + template + requires hasCompatibleStellarEquilibriumPhysicalRoot && + hasCompleteStellarEquilibriumRuntime && hasStellarEquilibriumCoreRuntime && + CompilableRootManifestFor< + std::remove_cvref_t, + CompiledStellarEquilibriumForm>> + class PreparedVariadicStellarEquilibriumOperator final : public mfem::Operator { + public: + using ModelType = std::remove_cvref_t; + using EquationOfStateType = model::EquationOfStateType; + using SurfaceConditionType = model::SurfaceConditionType; + using CoreRuntime = StellarEquilibriumCoreRuntime; + using PhysicalCoreType = StellarEquilibriumPhysicalCoreType; + using PhysicalCoreOwner = std::unique_ptr; + using SpecificationTypes = typename ModelType::SpecificationTypes; + using FormType = CompiledStellarEquilibriumForm; + using JacobianFormType = CompiledStellarEquilibriumJacobianForm; + using Layout = utils::blocks::form_layout; + using Manifest = EquilibriumSystemManifest; + using Report = PreparedVariadicStellarEquilibriumReport; + + static constexpr std::size_t rotationProviderCount = + stellarEquilibriumRotationProviderCount; + + /* + * The physical core may retain references to constitutive data, so a + * raw ModelType reference is intentionally not a construction option. + * Shared ownership keeps every EOS backend safe for the lifetime of + * this prepared root. StellarEquilibriumProblem owns and supplies the + * same handle on the normal user-facing path. + */ + PreparedVariadicStellarEquilibriumOperator( + fem::FEM &finiteElements, + const mapping::DomainMapper &domainMapper, + std::shared_ptr model, + PressureSurfaceConstraintView surfaceConstraint, + deformation::PreparedDomainDeformationRuntime domainDeformation + ) requires(rotationProviderCount <= 1) + : PreparedVariadicStellarEquilibriumOperator( + finiteElements, + domainMapper, + model, + MakePhysical( + finiteElements, + domainMapper, + RequireModel(model), + surfaceConstraint, + std::move(domainDeformation) + ) + ) { + } + + PreparedVariadicStellarEquilibriumOperator(const PreparedVariadicStellarEquilibriumOperator &) = delete; + PreparedVariadicStellarEquilibriumOperator &operator=(const PreparedVariadicStellarEquilibriumOperator &) = + delete; + PreparedVariadicStellarEquilibriumOperator(PreparedVariadicStellarEquilibriumOperator &&) = delete; + PreparedVariadicStellarEquilibriumOperator &operator=(PreparedVariadicStellarEquilibriumOperator &&) = delete; + + [[nodiscard]] Report Prepare( + const mfem::Vector &state, + const StellarEquilibriumDependencies &dependencies, + const physics::RigidRotation &rotation + ) requires(rotationProviderCount == 0) { + detail::StellarEquilibriumControlContext controls{ + .dependencies = dependencies, + .rotation = rotation, + .rotationProviderCount = 1, + .generatedPhysicalControl = false + }; + return PrepareWithControls(state, std::move(controls)); + } + + [[nodiscard]] Report Prepare( + const mfem::Vector &state, + const StellarEquilibriumDependencies &dependencies + ) requires(rotationProviderCount == 1) { + detail::StellarEquilibriumControlContext controls{ + .dependencies = dependencies, + .rotation = std::nullopt, + .rotationProviderCount = 0, + .generatedPhysicalControl = false + }; + return PrepareWithControls(state, std::move(controls)); + } + + void BuildResidual(mfem::Vector &residual) const { + VerifyPrepared(); + residual = m_cachedResidual; + } + + void Mult(const mfem::Vector &direction, mfem::Vector &action) const override { + VerifyPrepared(); + MFEM_VERIFY(direction.Size() == Width(), "The variadic stellar root received a wrong-sized direction."); + + GatherPhysicalValues( + direction, + m_physicalDirection, + typename PhysicalForm::value_blocks{} + ); + m_physical->Mult(m_physicalDirection, m_physicalAction); + + action.SetSize(Height()); + action = 0.0; + ScatterPhysicalResiduals( + m_physicalAction, + action, + typename PhysicalForm::residual_blocks{} + ); + + const auto directionView = m_manifest.directionView(direction); + const auto actionView = m_manifest.residualView(action); + m_specifications.AddJacobianAction(directionView, actionView, *m_physical); + } + + [[nodiscard]] bool IsPrepared() const noexcept { + return m_isPrepared && m_physical->IsPrepared() && m_specifications.IsPrepared(); + } + + [[nodiscard]] const Layout &GetLayout() const noexcept { + return m_manifest.layout(); + } + + [[nodiscard]] const Manifest &GetRootManifest() const noexcept { + return m_manifest; + } + + [[nodiscard]] const PhysicalCoreType &GetPhysicalOperator() const noexcept { + return *m_physical; + } + + template + requires ModelType::template containsSpecification + [[nodiscard]] const auto &GetPreparedContribution() const noexcept { + return m_specifications.template Get(); + } + + [[nodiscard]] const PreparedAngularMomentumOperator &GetAngularMomentumConstraint() const noexcept + requires ModelType::template containsSpecification { + return GetPreparedContribution().constraint(); + } + + [[nodiscard]] const PreparedCentralDensityConstraint &GetCentralDensityConstraint() const noexcept + requires ModelType::template containsSpecification { + return GetPreparedContribution().constraint(); + } + + [[nodiscard]] RootConstraintReport GetFixedMassReport() const { + VerifyPrepared(); + const RootConstraintReport physicalReport = m_physical->GetFixedMassReport(); + return m_manifest.fixedMassReport(physicalReport.achieved); + } + + [[nodiscard]] AngularMomentumConstraintReport GetAngularMomentumReport() const + requires ModelType::template containsSpecification { + VerifyPrepared(); + return GetAngularMomentumConstraint().GetConstraintReport(); + } + + [[nodiscard]] CentralDensityConstraintReport GetCentralDensityReport() const + requires ModelType::template containsSpecification { + VerifyPrepared(); + return GetCentralDensityConstraint().GetConstraintReport(); + } + + private: + using PhysicalForm = utils::blocks::surface_deformed_stellar_equilibrium_form; + using SpecificationSlots = + detail::PreparedSpecificationSet; + + [[nodiscard]] static const ModelType &RequireModel( + const std::shared_ptr &model + ) { + MFEM_VERIFY( + model != nullptr, + "The variadic stellar-equilibrium root requires shared ownership of its model." + ); + return *model; + } + + template + void GatherPhysicalValueBlock( + const mfem::Vector &root, + mfem::Vector &physical + ) const { + constexpr int rootIndex = + utils::blocks::type_index_v; + constexpr int physicalIndex = + utils::blocks::type_index_v; + const auto &rootOffsets = m_manifest.layout().value_offsets(); + const auto &physicalOffsets = m_physical->GetLayout().value_offsets(); + const int rootSize = rootOffsets[rootIndex + 1] - rootOffsets[rootIndex]; + const int physicalSize = physicalOffsets[physicalIndex + 1] - physicalOffsets[physicalIndex]; + MFEM_VERIFY(rootSize == physicalSize, "A compiled physical value block changed size in the root layout."); + const mfem::Vector source( + const_cast(root.GetData()) + rootOffsets[rootIndex], + rootSize + ); + mfem::Vector destination(physical, physicalOffsets[physicalIndex], physicalSize); + destination = source; + destination.SyncAliasMemory(physical); + } + + template + void GatherPhysicalValues( + const mfem::Vector &root, + mfem::Vector &physical, + utils::blocks::type_list + ) const { + MFEM_VERIFY(physical.Size() == m_physical->Width(), "The physical-state workspace has the wrong size."); + (GatherPhysicalValueBlock(root, physical), ...); + } + + template + void ScatterPhysicalResidualBlock( + const mfem::Vector &physical, + mfem::Vector &root + ) const { + constexpr int physicalIndex = + utils::blocks::type_index_v; + constexpr int rootIndex = + utils::blocks::type_index_v; + const auto &physicalOffsets = m_physical->GetLayout().residual_offsets(); + const auto &rootOffsets = m_manifest.layout().residual_offsets(); + const int physicalSize = physicalOffsets[physicalIndex + 1] - physicalOffsets[physicalIndex]; + const int rootSize = rootOffsets[rootIndex + 1] - rootOffsets[rootIndex]; + MFEM_VERIFY(rootSize == physicalSize, "A compiled physical residual block changed size in the root layout."); + const mfem::Vector source( + const_cast(physical.GetData()) + physicalOffsets[physicalIndex], + physicalSize + ); + mfem::Vector destination(root, rootOffsets[rootIndex], rootSize); + destination = source; + destination.SyncAliasMemory(root); + } + + template + void ScatterPhysicalResiduals( + const mfem::Vector &physical, + mfem::Vector &root, + utils::blocks::type_list + ) const { + MFEM_VERIFY(physical.Size() == m_physical->Height(), "The physical-action workspace has the wrong size."); + (ScatterPhysicalResidualBlock(physical, root), ...); + } + + [[nodiscard]] static PhysicalCoreOwner MakePhysical( + fem::FEM &finiteElements, + const mapping::DomainMapper &domainMapper, + const ModelType &model, + PressureSurfaceConstraintView surfaceConstraint, + deformation::PreparedDomainDeformationRuntime domainDeformation + ) { + return CoreRuntime::Make( + finiteElements, + domainMapper, + model.equationOfState(), + models::compileConstraint(model.template specification()), + surfaceConstraint, + std::move(domainDeformation) + ); + } + + [[nodiscard]] static std::array MakeValueSizes( + const StellarEquilibriumLayout &physicalLayout + ) { + return detail::MakeValueSizes::Apply(physicalLayout); + } + + [[nodiscard]] static std::array MakeResidualSizes( + const StellarEquilibriumLayout &physicalLayout + ) { + return detail::MakeResidualSizes::Apply(physicalLayout); + } + + PreparedVariadicStellarEquilibriumOperator( + fem::FEM &finiteElements, + const mapping::DomainMapper &domainMapper, + std::shared_ptr model, + PhysicalCoreOwner physical + ) + : mfem::Operator( + Layout(MakeValueSizes(physical->GetLayout()), MakeResidualSizes(physical->GetLayout())) + .residual_offsets() + .Last(), + Layout(MakeValueSizes(physical->GetLayout()), MakeResidualSizes(physical->GetLayout())) + .value_offsets() + .Last() + ), + m_model(std::move(model)), + m_physical(std::move(physical)), + m_specifications(finiteElements, domainMapper, *m_physical, *m_model), + m_manifest( + MakeValueSizes(m_physical->GetLayout()), + MakeResidualSizes(m_physical->GetLayout()), + *m_model, + CoreRuntime::SurfaceEquationCount(*m_physical) + ), + m_physicalState(m_physical->Width()), + m_physicalDirection(m_physical->Width()), + m_physicalAction(m_physical->Height()) { + static_assert(rotationProviderCount <= 1, "A stellar root cannot have two rigid-rotation providers."); + MFEM_VERIFY( + Width() == m_manifest.layout().value_offsets().Last() && + Height() == m_manifest.layout().residual_offsets().Last(), + "The variadic stellar root has inconsistent compiled dimensions." + ); + } + + [[nodiscard]] Report PrepareWithControls( + const mfem::Vector &state, + detail::StellarEquilibriumControlContext controls + ) { + MFEM_VERIFY(state.Size() == Width(), "The variadic stellar root received a wrong-sized state."); + const auto stateView = m_manifest.stateView(state); + + m_isPrepared = false; + m_specifications.ReadPhysicalControls(stateView, controls); + MFEM_VERIFY( + controls.rotationProviderCount == 1 && controls.rotation.has_value(), + "Exactly one rigid-rotation value must be supplied to the stellar physics core." + ); + + GatherPhysicalValues(state, m_physicalState, typename PhysicalForm::value_blocks{}); + Report report; + report.generatedPhysicalControl = controls.generatedPhysicalControl; + report.physical = m_physical->Prepare(m_physicalState, controls.dependencies, *controls.rotation); + m_specifications.template PrepareAfterPhysical<0>( + stateView, + controls.dependencies, + *m_physical, + report.specifications + ); + AssembleResidual(); + report.assembledResidual = true; + m_isPrepared = true; + return report; + } + + void AssembleResidual() { + mfem::Vector physicalResidual; + m_physical->BuildResidual(physicalResidual); + m_cachedResidual.SetSize(Height()); + m_cachedResidual = 0.0; + ScatterPhysicalResiduals( + physicalResidual, + m_cachedResidual, + typename PhysicalForm::residual_blocks{} + ); + const auto residualView = m_manifest.residualView(m_cachedResidual); + m_specifications.AddResidual(residualView, *m_physical); + } + + void VerifyPrepared() const { + MFEM_VERIFY(IsPrepared(), "The variadic stellar-equilibrium root must be prepared before application."); + } + + std::shared_ptr m_model; + PhysicalCoreOwner m_physical; + SpecificationSlots m_specifications; + Manifest m_manifest; + mfem::Vector m_physicalState; + mutable mfem::Vector m_physicalDirection; + mutable mfem::Vector m_physicalAction; + mfem::Vector m_cachedResidual; + bool m_isPrepared{false}; + }; +} // namespace mean_field::operators + +export namespace mean_field::stellar { + template + using DensityVolumeIntegralContext = + operators::DensityVolumeIntegralContext; +} // namespace mean_field::stellar diff --git a/libmeanfield/interface/operators/root_manifest.cppm b/libmeanfield/interface/operators/root_manifest.cppm index 825fdf0..350ec6a 100644 --- a/libmeanfield/interface/operators/root_manifest.cppm +++ b/libmeanfield/interface/operators/root_manifest.cppm @@ -10,21 +10,30 @@ module; #include #include #include +#include #include export module mean_field:operators.root_manifest; export import :model.compiled_fixed_mass; +export import :model.compiled_fixed_angular_momentum; export import :model.compiled_fixed_central_density; export import :model.specifications; +export import :operators.stellar_equilibrium_compiler; export import :utils.blocks; export namespace mean_field::operators { enum class RootBlockKind { value, residual }; enum class RootBlockProvenance { physical_operator, model_specification }; enum class RootRowInjection { physical_equation, append_global, replace_carrier_rows }; - enum class RootColumnPolicy { physical_state, existing_physical_multiplier, solver_border, no_column }; + enum class RootColumnPolicy { + physical_state, + existing_physical_multiplier, + generated_physical_coordinate, + solver_border, + no_column + }; enum class RootScalePolicy { unscaled, target_relative }; struct RootBlockDescriptor final { @@ -66,12 +75,6 @@ export namespace mean_field::operators { double residualScale; }; - struct CentralDensityManifestInput final { - double targetDensity; - double targetEnthalpy; - int centerDofCount; - }; - struct RootConstraintReport final { RootConstraintDescriptor descriptor; double achieved; @@ -80,6 +83,228 @@ export namespace mean_field::operators { }; namespace detail { + template + concept HasUniqueModelSpecificationRole = requires { + typename std::remove_cvref_t::SpecificationTypes; + requires models::HasUniqueSpecificationForRole< + Role, + typename std::remove_cvref_t::SpecificationTypes>; + }; + + template + requires HasUniqueModelSpecificationRole + using ModelSpecificationForRole = models::SpecificationForRoleT< + Role, + typename std::remove_cvref_t::SpecificationTypes>; + + template + concept AccessibleModelSpecificationRole = + HasUniqueModelSpecificationRole && + (requires(const std::remove_cvref_t &model) { + { + model.template specificationForRole() + } -> std::same_as &>; + } || requires(const std::remove_cvref_t &model) { + { + model.template specification>() + } -> std::same_as &>; + }); + + /* + * Model frontends may expose a convenient specificationForRole() + * accessor, while the lightweight compile-time model deliberately + * exposes only specification(). Keep manifest + * compilation independent of that presentation choice. + */ + template + requires AccessibleModelSpecificationRole + [[nodiscard]] const ModelSpecificationForRole & + modelSpecificationForRole(const Model &model) { + if constexpr (requires { + { + model.template specificationForRole() + } -> std::same_as &>; + }) { + return model.template specificationForRole(); + } else { + using Specification = ModelSpecificationForRole; + return model.template specification(); + } + } + + struct ManifestTargetData final { + double target; + std::optional carrierTarget; + double residualScale; + }; + + template + struct ManifestTargetAdapter { + static constexpr bool registered = false; + }; + + template + using DeclaredSpecificationManifest = + typename models::SpecificationContribution< + std::remove_cvref_t>::Manifest; + + template + [[nodiscard]] consteval bool genericManifestTargetIsComplete() { + using Manifest = DeclaredSpecificationManifest; + if constexpr (!requires { + typename Manifest::TargetQuantity; + typename Manifest::ConstraintResidualQuantity; + }) { + // Preserve the lower-level, structurally declared manifest + // path. It has no dimensional types from which a distinct + // residual reference could be inferred. + return true; + } else if constexpr (std::same_as< + typename Manifest::TargetQuantity, + typename Manifest::ConstraintResidualQuantity>) { + return true; + } else { + using ResidualReference = dimensions::QuantityValue< + typename Manifest::ConstraintResidualQuantity>; + return requires(const Specification &specification) { + { + specification.residualReference() + } -> std::same_as; + }; + } + } + + /* + * The ordinary physics-facing extension path is a strongly typed + * target(). If the constraint equation has different units, the + * specification also supplies a strongly typed residualReference(). + * This remains local physics vocabulary and prevents a target with, + * for example, density units from silently scaling an energy row. + */ + template + requires(!std::same_as< + std::remove_cvref_t, + models::FixedCentralDensity>) && + requires(const Specification &specification) { + { + specification.target().value() + } -> std::convertible_to; + } + struct ManifestTargetAdapter { + using Manifest = DeclaredSpecificationManifest; + + static constexpr bool registered = + genericManifestTargetIsComplete(); + + [[nodiscard]] static ManifestTargetData Read( + const Specification &specification, + const Model & + ) requires registered { + const double target = static_cast(specification.target().value()); + const double residualReference = [&] { + if constexpr (requires { + typename Manifest::TargetQuantity; + typename Manifest::ConstraintResidualQuantity; + }) { + if constexpr (!std::same_as< + typename Manifest::TargetQuantity, + typename Manifest::ConstraintResidualQuantity>) { + return static_cast( + specification.residualReference().value()); + } else { + return target; + } + } else { + return target; + } + }(); + return { + .target = target, + .carrierTarget = std::nullopt, + .residualScale = std::max( + std::abs(residualReference), + 1.0e-300) + }; + } + }; + + template + struct ManifestTargetAdapter { + static constexpr bool registered = true; + + [[nodiscard]] static ManifestTargetData Read( + const models::FixedTotalMass &specification, + const Model & + ) { + const double target = specification.targetMass().value(); + return { + .target = target, + .carrierTarget = target, + .residualScale = std::max(std::abs(target), 1.0e-300) + }; + } + }; + + template + struct ManifestTargetAdapter { + static constexpr bool registered = true; + + [[nodiscard]] static ManifestTargetData Read( + const models::FixedAngularMomentum &specification, + const Model & + ) { + const double target = specification.targetAngularMomentum().value(); + return { + .target = target, + .carrierTarget = std::nullopt, + .residualScale = std::max(std::abs(target), 1.0e-300) + }; + } + }; + + template + requires requires( + const models::FixedCentralDensity &specification, + const Model &model + ) { + { + models::compileConstraint( + specification, + modelSpecificationForRole(model) + ) + .targetDensity() + .value() + } -> std::convertible_to; + { + models::compileConstraint( + specification, + modelSpecificationForRole(model) + ) + .targetEnthalpy() + .value() + } -> std::convertible_to; + } + struct ManifestTargetAdapter { + static constexpr bool registered = true; + + [[nodiscard]] static ManifestTargetData Read( + const models::FixedCentralDensity &specification, + const Model &model + ) { + const auto compiled = models::compileConstraint( + specification, + modelSpecificationForRole(model) + ); + const double target = compiled.targetDensity().value(); + const double carrierTarget = compiled.targetEnthalpy().value(); + return { + .target = target, + .carrierTarget = carrierTarget, + .residualScale = std::max(std::abs(carrierTarget), 1.0e-300) + }; + } + }; + struct StaticRootBlockDescriptor final { std::string_view stableId; std::string_view symbol; @@ -92,6 +317,9 @@ export namespace mean_field::operators { template struct RootBlockTraits; + template + concept DescribedRootBlock = requires { RootBlockTraits::descriptor; }; + #define MEAN_FIELD_PHYSICAL_VALUE_BLOCK(BlockType, StableId, Symbol) \ template <> struct RootBlockTraits { \ static constexpr StaticRootBlockDescriptor descriptor{ \ @@ -183,176 +411,608 @@ export namespace mean_field::operators { #undef MEAN_FIELD_PHYSICAL_VALUE_BLOCK #undef MEAN_FIELD_PHYSICAL_RESIDUAL_BLOCK - template <> struct RootBlockTraits { + /* + * Generated block descriptors are constexpr objects. A third-party + * manifest can satisfy the broad, physics-facing manifest concept while + * publishing non-constant or empty metadata and merely claiming + * available=true. Detect both cases before selecting RootBlockTraits: + * attempting to initialize its constexpr descriptor first would turn a + * capability query into a hard template error. + */ + template + using ConstantCompleteGeneratedManifestMetadata = std::bool_constant< + !static_cast(Candidate::valueStableId).empty() && + !static_cast(Candidate::valueSymbol).empty() && + !static_cast(Candidate::residualStableId).empty() && + !static_cast(Candidate::residualSymbol).empty() && + !static_cast(Candidate::targetUnits).empty() && + !static_cast(Candidate::residualUnits).empty()>; + + template + struct CompleteGeneratedManifestMetadata : std::false_type { }; + + template + struct CompleteGeneratedManifestMetadata< + Candidate, + std::void_t>> + : ConstantCompleteGeneratedManifestMetadata { }; + + template + concept ManifestDescribedGeneratedCoordinate = requires { + typename Generated::SpecificationType; + } && models::ModelSpecification && + models::CompleteGeneratedManifestFor && + CompleteGeneratedManifestMetadata< + typename models::SpecificationContribution< + typename Generated::SpecificationType>::Manifest>::value; + + template + [[nodiscard]] consteval RootColumnPolicy generatedColumnPolicy() { + constexpr auto kind = models::SpecificationContribution::generatedStateKind; + if constexpr (kind == models::GeneratedStateKind::multiplier) { + return RootColumnPolicy::existing_physical_multiplier; + } else if constexpr (kind == models::GeneratedStateKind::physical_coordinate) { + return RootColumnPolicy::generated_physical_coordinate; + } else if constexpr (kind == models::GeneratedStateKind::solver_border) { + return RootColumnPolicy::solver_border; + } else { + return RootColumnPolicy::no_column; + } + } + + /* + * Generated blocks are described by their owning physics + * specification. This is the manifest analogue of the variadic + * operator compiler: adding a specification must not require another + * combination-specific block-traits specialization. + */ + template + struct RootBlockTraits> { + private: + using Specification = typename Generated::SpecificationType; + using Manifest = typename models::SpecificationContribution::Manifest; + + public: static constexpr StaticRootBlockDescriptor descriptor{ - "fixed_total_mass.multiplier", - "C", + Manifest::valueStableId, + Manifest::valueSymbol, RootBlockProvenance::model_specification, - "FixedTotalMass", + models::SpecificationTraits::name, RootRowInjection::physical_equation, - RootColumnPolicy::existing_physical_multiplier, + generatedColumnPolicy(), RootScalePolicy::unscaled }; }; - template <> struct RootBlockTraits { + template + struct RootBlockTraits> { + private: + using Specification = typename Generated::SpecificationType; + using Manifest = typename models::SpecificationContribution::Manifest; + + public: static constexpr StaticRootBlockDescriptor descriptor{ - "fixed_total_mass.residual", - "R_M", + Manifest::residualStableId, + Manifest::residualSymbol, RootBlockProvenance::model_specification, - "FixedTotalMass", + models::SpecificationTraits::name, RootRowInjection::append_global, RootColumnPolicy::no_column, RootScalePolicy::target_relative }; }; - template <> struct RootBlockTraits { - static constexpr StaticRootBlockDescriptor descriptor{ - "fixed_central_density.border", - "lambda_rho_c", - RootBlockProvenance::model_specification, - "FixedCentralDensity", - RootRowInjection::physical_equation, - RootColumnPolicy::solver_border, - RootScalePolicy::unscaled - }; + template + struct SingleGeneratedBlockPair; + + template + struct SingleGeneratedBlockPair< + models::ModelTypeList, + models::ModelTypeList> { + using Value = utils::blocks::generated_value_block; + using Residual = utils::blocks::generated_residual_block; }; - template <> struct RootBlockTraits { - static constexpr StaticRootBlockDescriptor descriptor{ - "fixed_central_density.residual", "R_rho_c", - RootBlockProvenance::model_specification, "FixedCentralDensity", - RootRowInjection::append_global, RootColumnPolicy::no_column, - RootScalePolicy::target_relative - }; + template + struct RootManifestSpecificationContribution { + private: + static constexpr auto role = models::SpecificationTraits::role; + static constexpr auto generatedValueArity = + models::SpecificationContribution::generatedValueArity; + static constexpr auto generatedResidualArity = + models::SpecificationContribution::generatedResidualArity; + + public: + static constexpr bool registered = + generatedValueArity == 0 && generatedResidualArity == 0 && + role != models::SpecificationRole::boundary_condition; + static constexpr std::size_t constraintCount = 0; + static constexpr std::size_t replacementCount = 0; + + template + static constexpr bool completeFor = registered; + + template + static void AppendConstraints( + std::array &, + std::size_t &, + const Model & + ) noexcept { + } + + template + static void AppendReplacements( + std::array &, + std::size_t &, + const Model &, + int + ) noexcept { + } }; - template - [[nodiscard]] constexpr double blockScale( - const double fixedMassScale, - const double centralDensityScale - ) noexcept { - if constexpr (std::same_as) { - return fixedMassScale; - } else if constexpr (std::same_as) { - return centralDensityScale; + template + requires( + models::SpecificationContribution::generatedValueArity == 1 && + models::SpecificationContribution::generatedResidualArity == 1 && + models::CompleteGeneratedManifestFor && + ManifestTargetAdapter::registered + ) + struct RootManifestSpecificationContribution { + private: + using Contribution = models::SpecificationContribution; + using GeneratedBlocks = SingleGeneratedBlockPair< + typename Contribution::GeneratedValues, + typename Contribution::GeneratedResiduals>; + + public: + using ValueBlock = typename GeneratedBlocks::Value; + using ResidualBlock = typename GeneratedBlocks::Residual; + using Manifest = typename Contribution::Manifest; + + static constexpr bool registered = true; + static constexpr std::size_t constraintCount = 1; + static constexpr std::size_t replacementCount = 0; + + template + static constexpr bool completeFor = + utils::blocks::contains_type_v && + utils::blocks::contains_type_v; + + template + static void AppendConstraints( + std::array &descriptors, + std::size_t &next, + const Model &model + ) { + const auto target = ManifestTargetAdapter::Read( + model.template specification(), model + ); + descriptors[next++] = { + .stableId = models::SpecificationTraits::name, + .role = models::SpecificationTraits::role, + .rowInjection = RootRowInjection::append_global, + .columnPolicy = generatedColumnPolicy(), + .valueBlock = utils::blocks::type_index_v, + .residualBlock = utils::blocks::type_index_v, + .rowArity = ResidualBlock::static_block_size, + .columnArity = ValueBlock::static_block_size, + .target = target.target, + .carrierTarget = target.carrierTarget, + .targetUnits = Manifest::targetUnits, + .residualUnits = Manifest::residualUnits, + .residualScale = target.residualScale + }; + } + + template + static void AppendReplacements( + std::array &, + std::size_t &, + const Model &, + int + ) noexcept { + } + }; + + /* + * Boundary equations replace rows rather than append a generated + * scalar. This is a per-physics-condition adapter, never a + * constraint-pack adapter. Future surface families register their + * carrier equation here (or through a later generalized surface + * compiler) and automatically compose with every integral/phase pack. + */ + template + requires requires(const Model &model) { + { + modelSpecificationForRole(model) + .targetPressure() + .value() + } -> std::convertible_to; + } + struct RootManifestSpecificationContribution { + static constexpr bool registered = true; + static constexpr std::size_t constraintCount = 1; + static constexpr std::size_t replacementCount = 1; + + template + static constexpr bool completeFor = utils::blocks::contains_type_v< + utils::blocks::enthalpy::specific::residual, + typename Form::residual_blocks>; + + template + static void AppendConstraints( + std::array &descriptors, + std::size_t &next, + const Model &model + ) { + descriptors[next++] = { + .stableId = models::SpecificationTraits::name, + .role = models::SpecificationRole::boundary_condition, + .rowInjection = RootRowInjection::replace_carrier_rows, + .columnPolicy = RootColumnPolicy::no_column, + .valueBlock = -1, + .residualBlock = utils::blocks::type_index_v< + utils::blocks::enthalpy::specific::residual, + typename Form::residual_blocks>, + .rowArity = 0, + .columnArity = 0, + .target = modelSpecificationForRole(model) + .targetPressure() + .value(), + .carrierTarget = std::nullopt, + .targetUnits = "pressure", + .residualUnits = "specific_enthalpy", + .residualScale = 1.0 + }; + } + + template + static void AppendReplacements( + std::array &descriptors, + std::size_t &next, + const Model &, + const int replacedRowCount + ) { + descriptors[next++] = { + .stableId = "isobaric_surface.replacement", + .sourceSpecification = models::SpecificationTraits::name, + .role = models::SpecificationRole::boundary_condition, + .carrierResidualBlock = utils::blocks::type_index_v< + utils::blocks::enthalpy::specific::residual, + typename Form::residual_blocks>, + .replacedRowCount = replacedRowCount + }; + } + }; + + template + struct CompileRootManifestContributions; + + template + struct CompileRootManifestContributions< + Model, + models::detail::SpecificationSetStorage> { + static constexpr bool complete = + (RootManifestSpecificationContribution::registered && ...); + static constexpr std::size_t constraintCount = + (std::size_t{0} + ... + + RootManifestSpecificationContribution::constraintCount); + static constexpr std::size_t replacementCount = + (std::size_t{0} + ... + + RootManifestSpecificationContribution::replacementCount); + + template + static constexpr bool completeFor = complete && + (RootManifestSpecificationContribution< + Specifications, + Model>::template completeFor && ...); + + template + [[nodiscard]] static std::array + MakeConstraints(const Model &model) { + std::array descriptors{}; + std::size_t next = 0; + (RootManifestSpecificationContribution::template AppendConstraints( + descriptors, next, model + ), ...); + return descriptors; + } + + template + [[nodiscard]] static std::array + MakeReplacements(const Model &model, const int replacedRowCount) { + std::array descriptors{}; + std::size_t next = 0; + (RootManifestSpecificationContribution::template AppendReplacements( + descriptors, next, model, replacedRowCount + ), ...); + return descriptors; + } + }; + + template struct AllRootBlocksAreDescribed; + + template + struct AllRootBlocksAreDescribed> + : std::bool_constant<(DescribedRootBlock && ...)> { }; + + template + [[nodiscard]] consteval bool rootBlockStableIdsAreUnique() { + constexpr std::array stableIds{ + RootBlockTraits::descriptor.stableId... + }; + for (std::size_t first = 0; first < stableIds.size(); ++first) { + for (std::size_t second = first + 1; second < stableIds.size(); ++second) { + if (stableIds[first] == stableIds[second]) { + return false; + } + } + } + return true; + } + + /* + * Stable IDs are machine identities within a block kind. Symbols are + * deliberately excluded: they are mathematical presentation labels + * and two independent constraints may reasonably use the same one. + */ + template struct RootBlockStableIdsAreUnique : std::false_type { }; + + template + requires(DescribedRootBlock && ...) + struct RootBlockStableIdsAreUnique> + : std::bool_constant()> { }; + + template struct GeneratedRootBlockOwner { + static constexpr bool available = false; + static constexpr bool isResidual = false; + }; + + template + requires requires { typename Generated::SpecificationType; } + struct GeneratedRootBlockOwner> { + using Specification = typename Generated::SpecificationType; + + static constexpr bool available = true; + static constexpr bool isResidual = false; + }; + + template + requires requires { typename Generated::SpecificationType; } + struct GeneratedRootBlockOwner> { + using Specification = typename Generated::SpecificationType; + + static constexpr bool available = true; + static constexpr bool isResidual = true; + }; + + template + [[nodiscard]] consteval bool rootBlockBelongsToModel() { + if constexpr (!GeneratedRootBlockOwner::available) { + return true; + } else { + using Specification = typename GeneratedRootBlockOwner::Specification; + return requires { + requires Model::template containsSpecification; + }; + } + } + + template struct AllGeneratedRootBlocksBelongToModel; + + template + struct AllGeneratedRootBlocksBelongToModel, Model> + : std::bool_constant<(rootBlockBelongsToModel() && ...)> { }; + + template + struct RootManifestCompilation { + static constexpr bool complete = false; + static constexpr std::size_t constraintCount = 0; + static constexpr std::size_t replacementCount = 0; + }; + + template + struct RootManifestCompilation< + Model, + Form, + std::void_t< + typename Model::SpecificationTypes, + typename Form::value_blocks, + typename Form::residual_blocks>> { + using Contributions = CompileRootManifestContributions< + Model, + typename Model::SpecificationTypes>; + + static constexpr bool complete = Contributions::template completeFor && + AllRootBlocksAreDescribed::value && + AllRootBlocksAreDescribed::value && + RootBlockStableIdsAreUnique::value && + RootBlockStableIdsAreUnique::value && + AllGeneratedRootBlocksBelongToModel< + typename Form::value_blocks, + Model>::value && + AllGeneratedRootBlocksBelongToModel< + typename Form::residual_blocks, + Model>::value; + static constexpr std::size_t constraintCount = Contributions::constraintCount; + static constexpr std::size_t replacementCount = Contributions::replacementCount; + }; + + template + [[nodiscard]] double modelBlockScale(const Model &model) { + if constexpr (GeneratedRootBlockOwner::isResidual) { + using Specification = typename GeneratedRootBlockOwner::Specification; + static_assert( + ManifestTargetAdapter::registered, + "A generated residual needs a physics target adapter before it can enter the root manifest." + ); + return ManifestTargetAdapter::Read( + model.template specification(), model + ).residualScale; } else { return 1.0; } } - template < - RootBlockKind Kind, - typename... Blocks> - [[nodiscard]] std::array< - RootBlockDescriptor, - sizeof...(Blocks)> - makeBlockDescriptors( + template + [[nodiscard]] std::array + makeModelBlockDescriptors( const mfem::Array &offsets, - const double fixedMassScale, - const double centralDensityScale, + const Model &model, utils::blocks::type_list ) { + static_assert( + (DescribedRootBlock && ...), + "Every compiled equilibrium block requires root-manifest metadata." + ); std::array descriptors{}; int index = 0; ((descriptors[index] = - {.stableId = RootBlockTraits::descriptor.stableId, - .symbol = RootBlockTraits::descriptor.symbol, - .kind = Kind, - .provenance = RootBlockTraits::descriptor.provenance, - .source = RootBlockTraits::descriptor.source, - .rowInjection = RootBlockTraits::descriptor.rowInjection, - .columnPolicy = RootBlockTraits::descriptor.columnPolicy, - .scalePolicy = RootBlockTraits::descriptor.scalePolicy, + {.stableId = RootBlockTraits::descriptor.stableId, + .symbol = RootBlockTraits::descriptor.symbol, + .kind = Kind, + .provenance = RootBlockTraits::descriptor.provenance, + .source = RootBlockTraits::descriptor.source, + .rowInjection = RootBlockTraits::descriptor.rowInjection, + .columnPolicy = RootBlockTraits::descriptor.columnPolicy, + .scalePolicy = RootBlockTraits::descriptor.scalePolicy, .canonicalIndex = index, - .offset = offsets[index], - .size = offsets[index + 1] - offsets[index], - .scale = blockScale(fixedMassScale, centralDensityScale)}, + .offset = offsets[index], + .size = offsets[index + 1] - offsets[index], + .scale = modelBlockScale(model)}, ++index), ...); return descriptors; } template - inline constexpr bool hasCentralDensity = Model::template containsSpecification; + inline constexpr std::size_t rootConstraintCount = + CompileRootManifestContributions::constraintCount; template - inline constexpr std::size_t rootConstraintCount = 2 + (hasCentralDensity ? 1 : 0); + inline constexpr std::size_t rootReplacementCount = + CompileRootManifestContributions::replacementCount; - template < - models::SpecifiedModelType Model, - typename Form> - [[nodiscard]] std::array< - RootConstraintDescriptor, - rootConstraintCount> - makeConstraintDescriptors( - const double targetMass, - const double targetSurfacePressure, - const double fixedMassScale, - const std::optional centralDensity - ) { - std::array> descriptors{}; - descriptors[0] = { - .stableId = "FixedTotalMass", - .role = models::SpecificationRole::invariant, - .rowInjection = RootRowInjection::append_global, - .columnPolicy = RootColumnPolicy::existing_physical_multiplier, - .valueBlock = models::FixedMassLayoutRequest::valueBlock().index, - .residualBlock = models::FixedMassLayoutRequest::residualBlock().index, - .rowArity = 1, - .columnArity = 1, - .target = targetMass, - .carrierTarget = targetMass, - .targetUnits = "mass", - .residualUnits = "mass", - .residualScale = fixedMassScale - }; - descriptors[1] = { - .stableId = "IsobaricSurface", - .role = models::SpecificationRole::boundary_condition, - .rowInjection = RootRowInjection::replace_carrier_rows, - .columnPolicy = RootColumnPolicy::no_column, - .valueBlock = -1, - .residualBlock = - utils::blocks::get_residual_block(utils::blocks::enthalpy_field.specific_term).index, - .rowArity = 0, - .columnArity = 0, - .target = targetSurfacePressure, - .carrierTarget = std::nullopt, - .targetUnits = "pressure", - .residualUnits = "specific_enthalpy", - .residualScale = 1.0 - }; - - if constexpr (hasCentralDensity) { - if (!centralDensity.has_value()) { - throw std::invalid_argument( - "A model containing FixedCentralDensity requires central-density manifest metadata." - ); - } - descriptors[2] = { - .stableId = "FixedCentralDensity", - .role = models::SpecificationRole::phase_condition, - .rowInjection = RootRowInjection::append_global, - .columnPolicy = RootColumnPolicy::solver_border, - .valueBlock = models::CentralDensityLayoutRequest::valueBlock().index, - .residualBlock = models::CentralDensityLayoutRequest::residualBlock().index, - .rowArity = 1, - .columnArity = 1, - .target = centralDensity->targetDensity, - .carrierTarget = centralDensity->targetEnthalpy, - .targetUnits = "density", - .residualUnits = "specific_enthalpy", - .residualScale = std::max(std::abs(centralDensity->targetEnthalpy), 1.0e-300) - }; - } else if (centralDensity.has_value()) { - throw std::invalid_argument( - "Central-density manifest metadata was provided to a model without FixedCentralDensity." - ); + template + [[nodiscard]] consteval bool manifestMatchesCompiledEquilibriumSystem() { + if constexpr (StellarEquilibriumSystemCompilable) { + return RootManifestCompilation< + std::remove_cvref_t, + std::remove_cvref_t>::complete && + std::same_as> && + std::same_as>; + } else { + return false; } - return descriptors; } + + } // namespace detail + + template + inline constexpr bool rootManifestIsCompilable = + detail::RootManifestCompilation< + std::remove_cvref_t, + std::remove_cvref_t>::complete; + + template + concept CompilableRootManifestFor = rootManifestIsCompilable; + + /* + * A model specification is addressable through specification() only + * when it contributes exactly one equation descriptor to the compiled + * root manifest. Membership in the model is deliberately insufficient: + * material laws such as an EOS participate in the physical operator, but + * do not own a root-constraint descriptor. + */ + template + concept RootManifestSpecificationDescriptorFor = + models::ModelSpecification> && + models::SpecifiedModelType> && + requires { + requires std::remove_cvref_t::template containsSpecification< + std::remove_cvref_t>; + requires detail::RootManifestSpecificationContribution< + std::remove_cvref_t, + std::remove_cvref_t>::registered; + requires detail::RootManifestSpecificationContribution< + std::remove_cvref_t, + std::remove_cvref_t>::constraintCount == 1; + }; + + /* + * A non-owning, read-only MFEM block. MFEM's aliasing Vector constructor + * requires a mutable pointer even for read-only use, so the writable alias + * stays private and only const operations are exposed. This prevents a + * residual/Jacobian contribution from mutating solver input through a + * nominally const state or direction view. + */ + class ReadOnlyVectorView final { + public: + ReadOnlyVectorView( + const mfem::Vector &vector, + const int offset, + const int size + ) + : m_view(CheckedData(vector, offset, size), size) { + } + + [[nodiscard]] int Size() const noexcept { + return m_view.Size(); + } + + [[nodiscard]] mfem::real_t operator()(const int index) const { + return m_view(index); + } + + [[nodiscard]] const mfem::real_t *GetData() const noexcept { + return m_view.GetData(); + } + + [[nodiscard]] double Norml2() const { + return m_view.Norml2(); + } + + [[nodiscard]] const mfem::Vector &asMFEMVector() const noexcept { + return m_view; + } + + [[nodiscard]] operator const mfem::Vector &() const noexcept { + return m_view; + } + + private: + [[nodiscard]] static mfem::real_t *CheckedData( + const mfem::Vector &vector, + const int offset, + const int size + ) { + if (offset < 0 || size < 0 || offset > vector.Size() - size) { + throw std::invalid_argument("A read-only block view received an invalid range."); + } + return const_cast(vector.GetData()) + offset; + } + + mfem::Vector m_view; + }; + + namespace detail { + template + struct RootValueTerm final { + using value = Block; + }; + + template + struct RootResidualTerm final { + using residual = Block; + }; + + template + struct SingleRootBlock; + + template + struct SingleRootBlock> final { + using Type = Block; + }; } // namespace detail template class RootStateView final { @@ -368,11 +1028,46 @@ export namespace mean_field::operators { } } - template [[nodiscard]] mfem::Vector block(const Term &term) const { + RootStateView( + mfem::Vector &&, + const utils::blocks::form_layout & + ) = delete; + + RootStateView( + const mfem::Vector &&, + const utils::blocks::form_layout & + ) = delete; + + RootStateView( + const mfem::Vector &, + utils::blocks::form_layout && + ) = delete; + + RootStateView( + const mfem::Vector &, + const utils::blocks::form_layout && + ) = delete; + + template [[nodiscard]] ReadOnlyVectorView block(const Term &term) const { constexpr auto valueBlock = utils::blocks::get_value_block(term); - return mfem::Vector( - const_cast(m_state.GetData()) + m_layout.offset(valueBlock), m_layout.size(valueBlock) - ); + return {m_state, m_layout.offset(valueBlock), m_layout.size(valueBlock)}; + } + + template + requires( + stellarEquilibriumSpecificationCompilationComplete && + StellarEquilibriumSpecificationCompilation:: + GeneratedValueBlocks::size == 1 && + utils::blocks::contains_type_v< + typename detail::SingleRootBlock< + typename StellarEquilibriumSpecificationCompilation< + Specification>::GeneratedValueBlocks>::Type, + typename Form::value_blocks>) + [[nodiscard]] ReadOnlyVectorView generatedCoordinate() const { + using Block = typename detail::SingleRootBlock< + typename StellarEquilibriumSpecificationCompilation< + Specification>::GeneratedValueBlocks>::Type; + return block(detail::RootValueTerm{}); } [[nodiscard]] const mfem::Vector &vector() const noexcept { @@ -384,6 +1079,68 @@ export namespace mean_field::operators { const utils::blocks::form_layout &m_layout; }; + template class MutableRootStateView final { + public: + MutableRootStateView( + mfem::Vector &state, + const utils::blocks::form_layout &layout + ) + : m_state(state), + m_layout(layout) { + if (state.Size() != layout.value_offsets().Last()) { + throw std::invalid_argument("MutableRootStateView received a vector with the wrong size."); + } + } + + MutableRootStateView( + mfem::Vector &&, + const utils::blocks::form_layout & + ) = delete; + + MutableRootStateView( + mfem::Vector &, + utils::blocks::form_layout && + ) = delete; + + MutableRootStateView( + mfem::Vector &, + const utils::blocks::form_layout && + ) = delete; + + template [[nodiscard]] mfem::Vector block(const Term &term) const { + constexpr auto valueBlock = utils::blocks::get_value_block(term); + return mfem::Vector( + m_state.GetData() + m_layout.offset(valueBlock), + m_layout.size(valueBlock) + ); + } + + template + requires( + stellarEquilibriumSpecificationCompilationComplete && + StellarEquilibriumSpecificationCompilation:: + GeneratedValueBlocks::size == 1 && + utils::blocks::contains_type_v< + typename detail::SingleRootBlock< + typename StellarEquilibriumSpecificationCompilation< + Specification>::GeneratedValueBlocks>::Type, + typename Form::value_blocks>) + [[nodiscard]] mfem::Vector generatedCoordinate() const { + using Block = typename detail::SingleRootBlock< + typename StellarEquilibriumSpecificationCompilation< + Specification>::GeneratedValueBlocks>::Type; + return block(detail::RootValueTerm{}); + } + + [[nodiscard]] mfem::Vector &vector() const noexcept { + return m_state; + } + + private: + mfem::Vector &m_state; + const utils::blocks::form_layout &m_layout; + }; + template class ResidualView final { public: ResidualView( @@ -397,11 +1154,43 @@ export namespace mean_field::operators { } } + ResidualView( + mfem::Vector &&, + const utils::blocks::form_layout & + ) = delete; + + ResidualView( + mfem::Vector &, + utils::blocks::form_layout && + ) = delete; + + ResidualView( + mfem::Vector &, + const utils::blocks::form_layout && + ) = delete; + template [[nodiscard]] mfem::Vector block(const Term &term) const { constexpr auto residualBlock = utils::blocks::get_residual_block(term); return mfem::Vector(m_residual.GetData() + m_layout.offset(residualBlock), m_layout.size(residualBlock)); } + template + requires( + stellarEquilibriumSpecificationCompilationComplete && + StellarEquilibriumSpecificationCompilation:: + GeneratedResidualBlocks::size == 1 && + utils::blocks::contains_type_v< + typename detail::SingleRootBlock< + typename StellarEquilibriumSpecificationCompilation< + Specification>::GeneratedResidualBlocks>::Type, + typename Form::residual_blocks>) + [[nodiscard]] mfem::Vector constraintResidual() const { + using Block = typename detail::SingleRootBlock< + typename StellarEquilibriumSpecificationCompilation< + Specification>::GeneratedResidualBlocks>::Type; + return block(detail::RootResidualTerm{}); + } + template void assign( const Term &term, @@ -412,6 +1201,7 @@ export namespace mean_field::operators { throw std::invalid_argument("ResidualView block assignment has the wrong size."); } destination = source; + destination.SyncAliasMemory(m_residual); } [[nodiscard]] mfem::Vector &vector() const noexcept { @@ -432,85 +1222,50 @@ export namespace mean_field::operators { using JacobianType = JacobianForm; using Layout = utils::blocks::form_layout; using StateView = RootStateView; + using MutableStateView = MutableRootStateView; using DirectionView = RootStateView; using RootResidualView = ResidualView; - static constexpr models::ModelCompilationClass compilationClass = Model::compilationClass; - static constexpr bool symbolicallySquare = Model::symbolicallySquare; + static constexpr bool hasCompleteEquilibriumCompiler = + detail::manifestMatchesCompiledEquilibriumSystem(); + static constexpr models::ModelCompilationClass compilationClass = + hasCompleteEquilibriumCompiler + ? models::EquilibriumSystemCompilation::complete_equilibrium_system + : models::EquilibriumSystemCompilation::equation_contributions_only; + static constexpr bool symbolicallySquare = Model::symbolicallySquare; + /* + * Every descriptor and scale is folded from the model's canonical + * specification pack. There is intentionally no scalar-input or + * constraint-combination constructor. + */ CompiledRootManifest( - const std::array< - int, - Form::value_block_count> &valueSizes, - const std::array< - int, - Form::residual_block_count> &residualSizes, - const double targetMass, - const double targetSurfacePressure, - const int replacedSurfaceRowCount, - const std::optional centralDensity = std::nullopt - ) - : m_layout( - valueSizes, - residualSizes - ), - m_fixedMassScale( - std::max( - std::abs(targetMass), - 1.0e-300 - ) - ), - m_centralDensityScale( - centralDensity.has_value() ? std::max( - std::abs(centralDensity->targetEnthalpy), - 1.0e-300 - ) - : 1.0 - ), - m_valueBlocks( - detail::makeBlockDescriptors( - m_layout.value_offsets(), - m_fixedMassScale, - m_centralDensityScale, - typename Form::value_blocks{} - ) - ), - m_residualBlocks( - detail::makeBlockDescriptors( - m_layout.residual_offsets(), - m_fixedMassScale, - m_centralDensityScale, - typename Form::residual_blocks{} - ) - ), - m_replacements{RootRowReplacementDescriptor{ - .stableId = "isobaric_surface.replacement", - .sourceSpecification = "IsobaricSurface", - .role = models::SpecificationRole::boundary_condition, - .carrierResidualBlock = - utils::blocks::get_residual_block(utils::blocks::enthalpy_field.specific_term).index, - .replacedRowCount = replacedSurfaceRowCount - }}, - m_constraints( - detail::makeConstraintDescriptors< - Model, - Form>( - targetMass, - targetSurfacePressure, - m_fixedMassScale, - centralDensity - ) - ) { + const std::array &valueSizes, + const std::array &residualSizes, + const Model &model, + const int replacedSurfaceRowCount + ) requires CompilableRootManifestFor + : m_layout(valueSizes, residualSizes), + m_valueBlocks(detail::makeModelBlockDescriptors( + m_layout.value_offsets(), model, typename Form::value_blocks{} + )), + m_residualBlocks(detail::makeModelBlockDescriptors( + m_layout.residual_offsets(), model, typename Form::residual_blocks{} + )), + m_replacements(detail::CompileRootManifestContributions< + Model, + typename Model::SpecificationTypes>::template MakeReplacements( + model, replacedSurfaceRowCount + )), + m_constraints(detail::CompileRootManifestContributions< + Model, + typename Model::SpecificationTypes>::template MakeConstraints(model)) { if (replacedSurfaceRowCount < 0) { throw std::invalid_argument( "An equilibrium-system manifest cannot contain a negative replacement-row count." ); } - if (centralDensity.has_value() && centralDensity->centerDofCount < 0) { - throw std::invalid_argument( - "An equilibrium-system manifest cannot contain a negative central-DOF count." - ); - } + ValidateNumericalMetadata(); if constexpr (compilationClass == models::EquilibriumSystemCompilation::complete_equilibrium_system) { if (m_layout.value_offsets().Last() != m_layout.residual_offsets().Last()) { throw std::invalid_argument( @@ -524,18 +1279,54 @@ export namespace mean_field::operators { return m_layout; } - [[nodiscard]] StateView stateView(const mfem::Vector &state) const { + [[nodiscard]] MutableStateView stateView(mfem::Vector &state) const & { return {state, m_layout}; } - [[nodiscard]] DirectionView directionView(const mfem::Vector &direction) const { + [[nodiscard]] StateView stateView(const mfem::Vector &state) const & { + return {state, m_layout}; + } + + [[nodiscard]] StateView stateView(mfem::Vector &&) const & = delete; + + [[nodiscard]] StateView stateView(const mfem::Vector &&) const & = delete; + + [[nodiscard]] MutableStateView stateView(mfem::Vector &) const && = delete; + + [[nodiscard]] StateView stateView(const mfem::Vector &) const && = delete; + + [[nodiscard]] StateView stateView(mfem::Vector &&) const && = delete; + + [[nodiscard]] StateView stateView(const mfem::Vector &&) const && = delete; + + [[nodiscard]] DirectionView directionView(const mfem::Vector &direction) const & { return {direction, m_layout}; } - [[nodiscard]] RootResidualView residualView(mfem::Vector &residual) const { + [[nodiscard]] DirectionView directionView(mfem::Vector &&) const & = delete; + + [[nodiscard]] DirectionView directionView(const mfem::Vector &&) const & = delete; + + [[nodiscard]] DirectionView directionView(const mfem::Vector &) const && = delete; + + [[nodiscard]] DirectionView directionView(mfem::Vector &&) const && = delete; + + [[nodiscard]] DirectionView directionView(const mfem::Vector &&) const && = delete; + + [[nodiscard]] RootResidualView residualView(mfem::Vector &residual) const & { return {residual, m_layout}; } + [[nodiscard]] RootResidualView residualView(mfem::Vector &&) const & = delete; + + [[nodiscard]] RootResidualView residualView(const mfem::Vector &&) const & = delete; + + [[nodiscard]] RootResidualView residualView(mfem::Vector &) const && = delete; + + [[nodiscard]] RootResidualView residualView(mfem::Vector &&) const && = delete; + + [[nodiscard]] RootResidualView residualView(const mfem::Vector &&) const && = delete; + [[nodiscard]] std::span valueBlocks() const noexcept { return m_valueBlocks; } @@ -552,14 +1343,34 @@ export namespace mean_field::operators { return m_constraints; } + template + requires RootManifestSpecificationDescriptorFor + [[nodiscard]] const RootConstraintDescriptor &specification() const { + constexpr auto requestedKey = models::SpecificationTraits::key; + const auto descriptor = std::find_if( + m_constraints.begin(), + m_constraints.end(), + [requestedKey](const RootConstraintDescriptor &candidate) { + return candidate.role == requestedKey.role && + candidate.stableId == requestedKey.stableName; + } + ); + if (descriptor == m_constraints.end()) { + throw std::logic_error( + "The requested model specification has no root-manifest equation descriptor." + ); + } + return *descriptor; + } + [[nodiscard]] static constexpr std::span specificationDescriptors() noexcept { return Model::runtimeSpecificationDescriptors(); } [[nodiscard]] RootConstraintReport fixedMassReport(const double achievedMass) const { - const RootConstraintDescriptor &descriptor = m_constraints[0]; - const double residual = achievedMass - descriptor.target; + const RootConstraintDescriptor &descriptor = specification(); + const double residual = achievedMass - descriptor.target; return { .descriptor = descriptor, .achieved = achievedMass, @@ -569,12 +1380,44 @@ export namespace mean_field::operators { } private: + void ValidateNumericalMetadata() const { + for (const RootConstraintDescriptor &constraint : m_constraints) { + if (!std::isfinite(constraint.target)) { + throw std::invalid_argument( + "An equilibrium-system manifest constraint requires a finite target." + ); + } + if (constraint.carrierTarget.has_value() && !std::isfinite(*constraint.carrierTarget)) { + throw std::invalid_argument( + "An equilibrium-system manifest constraint requires a finite carrier target." + ); + } + if (!std::isfinite(constraint.residualScale) || constraint.residualScale <= 0.0) { + throw std::invalid_argument( + "An equilibrium-system manifest constraint requires a finite, positive residual scale." + ); + } + } + for (const RootBlockDescriptor &block : m_valueBlocks) { + if (!std::isfinite(block.scale) || block.scale <= 0.0) { + throw std::invalid_argument( + "An equilibrium-system manifest block requires a finite, positive scale." + ); + } + } + for (const RootBlockDescriptor &block : m_residualBlocks) { + if (!std::isfinite(block.scale) || block.scale <= 0.0) { + throw std::invalid_argument( + "An equilibrium-system manifest block requires a finite, positive scale." + ); + } + } + } + Layout m_layout; - double m_fixedMassScale; - double m_centralDensityScale; std::array m_valueBlocks; std::array m_residualBlocks; - std::array m_replacements; + std::array> m_replacements; std::array> m_constraints; }; @@ -591,7 +1434,9 @@ export namespace mean_field::operators { using EquilibriumSpecificationDescriptor = RootConstraintDescriptor; using EquilibriumSpecificationReport = RootConstraintReport; - template using EquilibriumStateView = RootStateView; + template using EquilibriumStateView = RootStateView; + + template using MutableEquilibriumStateView = MutableRootStateView; template using EquilibriumResidualView = ResidualView; diff --git a/libmeanfield/interface/operators/stellar_equilibrium_compiler.cppm b/libmeanfield/interface/operators/stellar_equilibrium_compiler.cppm new file mode 100644 index 0000000..e3fa041 --- /dev/null +++ b/libmeanfield/interface/operators/stellar_equilibrium_compiler.cppm @@ -0,0 +1,815 @@ +module; + +#include +#include + +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 +struct StellarEquilibriumJacobianCoupling final { + using Residual = ResidualBlock; + using Value = ValueBlock; + + using ResidualBlockType = ResidualBlock; + using ValueBlockType = ValueBlock; +}; + +template +using EquilibriumJacobianCoupling = + StellarEquilibriumJacobianCoupling; + +namespace detail { +template struct ConcatenateBlockLists; + +template <> struct ConcatenateBlockLists<> { + using Type = utils::blocks::type_list<>; +}; + +template +struct ConcatenateBlockLists> { + using Type = utils::blocks::type_list; +}; + +template +struct ConcatenateBlockLists, + utils::blocks::type_list, + Remaining...> { + using Type = typename ConcatenateBlockLists< + utils::blocks::type_list, Remaining...>::Type; +}; + +template +using ConcatenateBlockListsT = typename ConcatenateBlockLists::Type; + +template struct AppendUniqueBlockType; + +template +struct AppendUniqueBlockType, Type> { + using TypeValue = std::conditional_t< + utils::blocks::contains_type_v>, + utils::blocks::type_list, + utils::blocks::type_list>; +}; + +template struct UniqueBlockListImpl; + +template +struct UniqueBlockListImpl> { + using Type = Accumulated; +}; + +template +struct UniqueBlockListImpl> { + using Type = typename UniqueBlockListImpl< + typename AppendUniqueBlockType::TypeValue, + utils::blocks::type_list>::Type; +}; + +template +using UniqueBlockListT = + typename UniqueBlockListImpl, List>::Type; + +template +using UniqueConcatenateBlockListsT = + UniqueBlockListT>; + +template struct IsValueBlockList : std::false_type {}; + +template +struct IsValueBlockList> + : std::bool_constant< + (std::derived_from && ...) && + utils::blocks::types_are_unique_v< + utils::blocks::type_list>> {}; + +template struct IsResidualBlockList : std::false_type {}; + +template +struct IsResidualBlockList> + : std::bool_constant< + (std::derived_from && + ...) && + utils::blocks::types_are_unique_v< + utils::blocks::type_list>> {}; + +template struct GeneratedValueBlocksFor; + +template +struct GeneratedValueBlocksFor> { + using Type = utils::blocks::type_list< + utils::blocks::generated_value_block...>; +}; + +template struct GeneratedResidualBlocksFor; + +template +struct GeneratedResidualBlocksFor< + models::ModelTypeList> { + using Type = utils::blocks::type_list< + utils::blocks::generated_residual_block...>; +}; + +/* + * 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 +struct UnmappedStellarDependency final {}; + +template +struct SingleGeneratedBlock { + using Type = UnmappedStellarDependency; + static constexpr bool available = false; +}; + +template +struct SingleGeneratedBlock, + DeclaredDependency> { + using Type = Block; + static constexpr bool available = true; +}; + +template +struct StellarDependencyBlock { + using Type = UnmappedStellarDependency; + static constexpr bool mapped = false; +}; + +template + requires(std::derived_from || + std::derived_from) +struct StellarDependencyBlock { + using Type = Block; + static constexpr bool mapped = true; +}; + +template +struct StellarDependencyBlock { + using Type = utils::blocks::density::mass::value; + static constexpr bool mapped = true; +}; + +template +struct StellarDependencyBlock { + using Type = utils::blocks::surface_deformation::parameters::value; + static constexpr bool mapped = true; +}; + +template +struct StellarDependencyBlock { + using Type = utils::blocks::gravity::gradient::value; + static constexpr bool mapped = true; +}; + +template +struct StellarDependencyBlock< + Specification, models::stellar::state::GravitationalPotential> { + using Type = utils::blocks::gravity::poisson::value; + static constexpr bool mapped = true; +}; + +template +struct StellarDependencyBlock { + using Type = utils::blocks::enthalpy::specific::value; + static constexpr bool mapped = true; +}; + +template +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 +struct StellarDependencyBlock< + Specification, models::stellar::state::GeneratedCoordinateOf> { +private: + using GeneratedBlocks = typename GeneratedValueBlocksFor< + typename models::SpecificationContribution::GeneratedValues>::Type; + using Dependency = models::stellar::state::GeneratedCoordinateOf; + using Selection = SingleGeneratedBlock; + +public: + using Type = typename Selection::Type; + static constexpr bool mapped = Selection::available; +}; + +template +struct StellarDependencyBlock< + Specification, models::stellar::equation::GravityGradientDefinition> { + using Type = utils::blocks::gravity::gradient::residual; + static constexpr bool mapped = true; +}; + +template +struct StellarDependencyBlock { + using Type = utils::blocks::gravity::poisson::residual; + static constexpr bool mapped = true; +}; + +template +struct StellarDependencyBlock { + using Type = utils::blocks::density::mass::residual; + static constexpr bool mapped = true; +}; + +template +struct StellarDependencyBlock< + Specification, models::stellar::equation::SurfaceShapeBalance> { + using Type = + utils::blocks::surface_deformation::shape_equilibrium::residual; + static constexpr bool mapped = true; +}; + +template +struct StellarDependencyBlock< + Specification, models::stellar::equation::HydrostaticBalance> { + using Type = utils::blocks::enthalpy::specific::residual; + static constexpr bool mapped = true; +}; + +template +struct StellarDependencyBlock { +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 +struct StellarDependencyBlock< + Specification, models::stellar::equation::ConstraintOf> { +private: + using GeneratedBlocks = typename GeneratedResidualBlocksFor< + typename models::SpecificationContribution::GeneratedResiduals>::Type; + using Dependency = models::stellar::equation::ConstraintOf; + using Selection = SingleGeneratedBlock; + +public: + using Type = typename Selection::Type; + static constexpr bool mapped = Selection::available; +}; + +template +struct CompileStellarDependencies { + using Type = utils::blocks::type_list< + UnmappedStellarDependency>; + static constexpr bool complete = false; +}; + +template +struct CompileStellarDependencies> { + using Type = utils::blocks::type_list< + typename StellarDependencyBlock::Type...>; + static constexpr bool complete = + (StellarDependencyBlock::mapped && ...); +}; + +template struct CoupleResidualToValues; + +template +struct CoupleResidualToValues> { + using Type = utils::blocks::type_list< + StellarEquilibriumJacobianCoupling...>; +}; + +template +struct CartesianJacobianCouplings; + +template +struct CartesianJacobianCouplings, + Values> { + using Type = ConcatenateBlockListsT< + typename CoupleResidualToValues::Type...>; +}; + +template struct IsJacobianCoupling : std::false_type {}; + +template +struct IsJacobianCoupling> + : std::bool_constant< + std::derived_from && + std::derived_from> {}; + +template +struct IsJacobianCouplingList : std::false_type {}; + +template +struct IsJacobianCouplingList> + : std::bool_constant<(IsJacobianCoupling::value && ...) && + utils::blocks::types_are_unique_v< + utils::blocks::type_list>> {}; + +template struct IsGeneratedValueBlock : std::false_type {}; + +template +struct IsGeneratedValueBlock> + : std::true_type {}; + +template +struct IsGeneratedResidualBlock : std::false_type {}; + +template +struct IsGeneratedResidualBlock> + : std::true_type {}; + +template +inline constexpr bool isGeneratedBorderIncidentCoupling = + IsGeneratedValueBlock::value || + IsGeneratedResidualBlock::value; + +template struct GeneratedBorderIncidentCouplings; + +template <> +struct GeneratedBorderIncidentCouplings> { + using Type = utils::blocks::type_list<>; +}; + +template +struct GeneratedBorderIncidentCouplings< + utils::blocks::type_list> { +private: + using Remaining = typename GeneratedBorderIncidentCouplings< + utils::blocks::type_list>::Type; + +public: + using Type = std::conditional_t< + isGeneratedBorderIncidentCoupling, + ConcatenateBlockListsT, Remaining>, + Remaining>; +}; + +template +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::Type; + using ChangedEquationJacobianCouplings = + typename CartesianJacobianCouplings::Type; + + // Compatibility names retained for backend code that distinguishes the + // generated row from the generated-coordinate column. + using GeneratedRowJacobianCouplings = ConstraintJacobianCouplings; + using AffectedRowJacobianCouplings = + typename CartesianJacobianCouplings::Type; + using AffectedStateJacobianCouplings = + typename CartesianJacobianCouplings::Type; + + using JacobianCouplings = UniqueConcatenateBlockListsT< + ConstraintJacobianCouplings, ChangedEquationJacobianCouplings>; + using IncidentJacobianCouplings = + typename GeneratedBorderIncidentCouplings::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::value && + IsResidualBlockList::value && + IsValueBlockList::value && + IsResidualBlockList::value && + IsJacobianCouplingList::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 +struct SelfDescribingSpecificationCompilationInputs { + using Contribution = models::SpecificationContribution; + using DependsOn = + CompileStellarDependencies; + using Affects = + CompileStellarDependencies; + + 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 +struct SelfDescribingSpecificationCompilation + : DeclarativeStellarEquilibriumSpecificationCompilation< + SelfDescribingSpecificationCompilationInputs::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 +struct StellarEquilibriumSpecificationCompilation + : detail::EmptySpecificationCompilation {}; + +template +struct StellarEquilibriumSpecificationCompilation + : detail::SelfDescribingSpecificationCompilation {}; + +namespace detail { +template +struct SpecificationCompilationIsComplete : std::false_type {}; + +template +struct SpecificationCompilationIsComplete< + Specification, + std::void_t::GeneratedValueBlocks, + typename StellarEquilibriumSpecificationCompilation< + Specification>::GeneratedResidualBlocks, + typename StellarEquilibriumSpecificationCompilation< + Specification>::DependsOnValueBlocks, + typename StellarEquilibriumSpecificationCompilation< + Specification>::AffectedResidualBlocks, + typename StellarEquilibriumSpecificationCompilation< + Specification>::JacobianCouplings, + std::bool_constant::registered>, + std::bool_constant::complete>>> + : std::bool_constant< + StellarEquilibriumSpecificationCompilation< + Specification>::registered && + StellarEquilibriumSpecificationCompilation::complete && + IsValueBlockList::GeneratedValueBlocks>::value && + IsResidualBlockList< + typename StellarEquilibriumSpecificationCompilation< + Specification>::GeneratedResidualBlocks>::value && + IsValueBlockList::DependsOnValueBlocks>::value && + IsResidualBlockList< + typename StellarEquilibriumSpecificationCompilation< + Specification>::AffectedResidualBlocks>::value && + IsJacobianCouplingList< + typename StellarEquilibriumSpecificationCompilation< + Specification>::JacobianCouplings>::value> {}; +} // namespace detail + +template +inline constexpr bool stellarEquilibriumSpecificationCompilationComplete = + detail::SpecificationCompilationIsComplete< + std::remove_cvref_t>::value; + +template +concept StellarEquilibriumSpecificationCompilable = + stellarEquilibriumSpecificationCompilationComplete; + +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 struct JacobianRowCouplings; + +template +struct JacobianRowCouplings> { + using Type = utils::blocks::type_list< + StellarEquilibriumJacobianCoupling...>; +}; + +template struct FlattenJacobianRows; + +template +struct FlattenJacobianRows> { + using Type = + ConcatenateBlockListsT::Type...>; +}; + +using StellarPhysicsJacobianCouplings = + typename FlattenJacobianRows::Type; + +template +struct SpecificationSetCompilationsAreComplete; + +template +struct SpecificationSetCompilationsAreComplete< + models::detail::SpecificationSetStorage> + : std::bool_constant<( + stellarEquilibriumSpecificationCompilationComplete && + ...)> {}; + +template +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 +struct CollectStellarEquilibriumContributionsImpl< + models::detail::SpecificationSetStorage, 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 +using CollectStellarEquilibriumContributions = + CollectStellarEquilibriumContributionsImpl< + SpecificationSet, + SpecificationSetCompilationsAreComplete::value>; + +template struct ValuesCoupledToResidual; + +template +struct ValuesCoupledToResidual> { + using Type = utils::blocks::type_list<>; +}; + +template +struct ValuesCoupledToResidual< + Residual, + utils::blocks::type_list< + StellarEquilibriumJacobianCoupling, Tail...>> { +private: + using Remaining = + typename ValuesCoupledToResidual>::Type; + +public: + using Type = std::conditional_t< + std::same_as, + ConcatenateBlockListsT, Remaining>, + Remaining>; +}; + +template struct MakeJacobianRow; + +template +struct MakeJacobianRow> { + using Type = utils::blocks::block_row; +}; + +template struct SynthesizeJacobianRows; + +template +struct SynthesizeJacobianRows, + Couplings> { + using Type = utils::blocks::type_list::Type>::Type...>; +}; + +template +struct CouplingEndpointsBelongToForm : std::false_type {}; + +template +struct CouplingEndpointsBelongToForm, + ValueBlocks, ResidualBlocks> + : std::bool_constant<((utils::blocks::contains_type_v< + typename Couplings::Value, ValueBlocks> && + utils::blocks::contains_type_v< + typename Couplings::Residual, ResidualBlocks>) && + ...)> {}; + +template 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 +struct CompileStellarEquilibriumSystem { + using ModelType = std::remove_cvref_t; + using Contributions = CollectStellarEquilibriumContributions< + typename ModelType::SpecificationTypes>; + + using GeneratedValueBlocks = typename Contributions::GeneratedValueBlocks; + using GeneratedCorrectionBlocks = GeneratedValueBlocks; + using GeneratedResidualBlocks = + typename Contributions::GeneratedResidualBlocks; + + using ValueBlocks = + ConcatenateBlockListsT; + using ResidualBlocks = ConcatenateBlockListsT; + using FormType = utils::blocks::block_form; + + using BaseJacobianCouplings = StellarPhysicsJacobianCouplings; + using ContributionJacobianCouplings = + typename Contributions::ContributionJacobianCouplings; + using IncidentJacobianCouplings = ContributionJacobianCouplings; + using JacobianCouplings = + UniqueConcatenateBlockListsT; + + // Pass two: materialize rows only after all contributed blocks are + // present in the final form. + using JacobianType = + typename SynthesizeJacobianRows::Type; + + static constexpr bool compilable = + Contributions::complete && + utils::blocks::block_form_is_valid_v && + IsJacobianCouplingList::value && + CouplingEndpointsBelongToForm::value && + utils::blocks::jacobian_form_is_valid_v; +}; +} // namespace detail + +template +inline constexpr bool stellarEquilibriumSystemIsCompilable = + detail::CompileStellarEquilibriumSystem< + std::remove_cvref_t>::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 +inline constexpr bool stellarEquilibriumIsSymbolicallyCompilable = + stellarEquilibriumSystemIsCompilable; + +template +concept StellarEquilibriumSymbolicallyCompilable = + stellarEquilibriumIsSymbolicallyCompilable; + +template +concept StellarEquilibriumSystemCompilable = + StellarEquilibriumSymbolicallyCompilable; + +template + requires StellarEquilibriumSystemCompilable +struct CompiledStellarEquilibriumSystem final + : detail::CompileStellarEquilibriumSystem> { + using Base = + detail::CompileStellarEquilibriumSystem>; + + 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); + static_assert(utils::blocks::valid_jacobian_form); +}; + +template + requires StellarEquilibriumSystemCompilable +using CompiledStellarEquilibriumForm = + typename CompiledStellarEquilibriumSystem::FormType; + +template + requires StellarEquilibriumSystemCompilable +using CompiledStellarEquilibriumJacobianForm = + typename CompiledStellarEquilibriumSystem::JacobianType; +} // namespace mean_field::operators diff --git a/libmeanfield/interface/operators/stellar_equilibrium_problem.cppm b/libmeanfield/interface/operators/stellar_equilibrium_problem.cppm index eb2c352..c189e4a 100644 --- a/libmeanfield/interface/operators/stellar_equilibrium_problem.cppm +++ b/libmeanfield/interface/operators/stellar_equilibrium_problem.cppm @@ -2,6 +2,8 @@ module; #include #include +#include +#include #include #include @@ -13,46 +15,126 @@ export import :deformation.domain_deformation; export import :equilibrium.stellar_discretization; export import :material.thermodynamic_equations; 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 namespace mean_field::equilibrium { + namespace detail { + template < + model::StellarModelType Model, + bool SymbolicallyCompilable = operators::StellarEquilibriumSystemCompilable> + struct StellarSurfaceCompilationAudit { + static constexpr bool complete = false; + }; + + template + struct StellarSurfaceCompilationAudit { + private: + using ModelType = std::remove_cvref_t; + using EquationOfState = typename ModelType::EquationOfStateType; + using Form = operators::CompiledStellarEquilibriumForm; + using AvailableEquations = material::StellarEquilibriumThermodynamicEquations; + + static constexpr bool thermodynamicsCompilable = + material::ThermodynamicEquationsCompilable; + + public: + static constexpr bool complete = [] { + if constexpr (!thermodynamicsCompilable) { + return false; + } else { + using ThermodynamicEquations = + material::CompiledThermodynamicEquationsT; + using Formulation = typename ThermodynamicEquations::PressureSurfaceFormulation; + using CompiledSurface = + surface::CompiledPressureSurfaceConstraintT; + return requires(const ModelType &model) { + { + surface::compilePressureSurfaceConstraint( + model.surfaceCondition(), + model.equationOfState() + ) + } -> std::same_as; + }; + } + }(); + }; + } // namespace detail + + template + inline constexpr bool hasStellarEquilibriumSurfaceCompilation = + detail::StellarSurfaceCompilationAudit>::complete; + template concept StellarEquilibriumModel = model::StellarModelType && requires { - requires std::remove_cvref_t::template containsSpecification; - requires std::remove_cvref_t::template containsSpecification; + typename std::remove_cvref_t::EquationOfStateType; + requires( + std::remove_cvref_t::template specificationRoleCount< + models::SpecificationRole::boundary_condition> == 1 + ); requires std::remove_cvref_t::template containsSpecification; - requires std::remove_cvref_t::specificationCount == - 3 + static_cast( - std::remove_cvref_t::template containsSpecification - ); + requires operators::StellarEquilibriumSystemCompilable>; + requires hasStellarEquilibriumSurfaceCompilation>; + requires operators::CompilableRootManifestFor< + std::remove_cvref_t, + operators::CompiledStellarEquilibriumForm>>; + requires operators::hasStellarEquilibriumCoreRuntime>; + requires operators::hasCompleteStellarEquilibriumRuntime>; + requires operators::stellarEquilibriumRotationProviderCount> <= 1; }; - template class StellarEquilibriumProblem final { + namespace detail { + template + struct StellarEquilibriumModelDiscretizationStructureAudit : std::false_type { }; + + template + requires StellarEquilibriumModel> && + StellarDiscretizationType> + struct StellarEquilibriumModelDiscretizationStructureAudit< + Model, + Discretization, + std::void_t< + typename std::remove_cvref_t::NormalizationPrescriptionType, + typename std::remove_cvref_t::SpecificationTypes, + operators::CompiledStellarEquilibriumForm>, + operators::StellarEquilibriumPhysicalCoreType>>> + : std::bool_constant::NormalizationPrescriptionType, + operators::CompiledStellarEquilibriumForm>, + operators::StellarEquilibriumPhysicalCoreType>, + typename std::remove_cvref_t::SpecificationTypes>> { }; + + struct StellarEquilibriumProblemFactory; + } // namespace detail + + template < + StellarEquilibriumModel Model, + StellarDiscretizationType Discretization = StellarDiscretization> + requires detail::StellarEquilibriumModelDiscretizationStructureAudit< + std::remove_cvref_t, + std::remove_cvref_t>::value + class StellarEquilibriumProblem final { public: using ModelType = std::remove_cvref_t; + using DiscretizationType = std::remove_cvref_t; + using NormalizationPrescriptionType = typename DiscretizationType::NormalizationPrescriptionType; static constexpr bool hasFixedCentralDensity = ModelType::template containsSpecification; + static constexpr bool hasFixedAngularMomentum = + ModelType::template containsSpecification; + static constexpr std::size_t generatedRotationProviderCount = + operators::stellarEquilibriumRotationProviderCount; static constexpr bool symbolicallySquare = ModelType::symbolicallySquare; - using PreparedOperatorType = std::conditional_t< - hasFixedCentralDensity, - operators::PreparedCentralDensityStellarEquilibriumOperator, - operators::PreparedStellarEquilibriumOperator>; - using FormType = std::conditional_t< - hasFixedCentralDensity, - operators::CentralDensityStellarEquilibriumForm, - 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 PreparedOperatorType = operators::PreparedVariadicStellarEquilibriumOperator; + using PhysicalCoreType = typename PreparedOperatorType::PhysicalCoreType; + using FormType = operators::CompiledStellarEquilibriumForm; + using JacobianFormType = operators::CompiledStellarEquilibriumJacobianForm; + using ManifestType = operators::EquilibriumSystemManifest; + using EquationOfStateType = model::EquationOfStateType; + using SurfaceConditionType = model::SurfaceConditionType; using AvailableThermodynamicEquations = material::StellarEquilibriumThermodynamicEquations; using ThermodynamicEquationsType = material::CompiledThermodynamicEquationsT; @@ -60,44 +142,22 @@ export namespace mean_field::equilibrium { typename ThermodynamicEquationsType::PressureSurfaceFormulation, EquationOfStateType>; - StellarEquilibriumProblem( - ModelType stellarModel, - 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(), - models::compileConstraint(m_stellarModel.template specification()), - operators::PressureSurfaceConstraintView{m_compiledSurfaceConstraint}, - CompileDefaultDomainDeformation(m_discretization.finiteElementModel()) - ) { - VerifyProblem(); - } + private: + friend struct detail::StellarEquilibriumProblemFactory; StellarEquilibriumProblem( ModelType stellarModel, - const StellarDiscretization discretization + DiscretizationType discretization ) - requires hasFixedCentralDensity - : m_stellarModel(std::move(stellarModel)), - m_discretization(discretization), - m_compiledSurfaceConstraint(CompileSurfaceConstraint(m_stellarModel)), + : m_stellarModel(std::make_shared(std::move(stellarModel))), + m_discretization(std::move(discretization)), + m_compiledSurfaceConstraint(CompileSurfaceConstraint(*m_stellarModel)), m_preparedOperator( m_discretization.finiteElementModel(), m_discretization.domainMapper(), - m_stellarModel.template specification(), - models::compileConstraint(m_stellarModel.template specification()), + m_stellarModel, operators::PressureSurfaceConstraintView{m_compiledSurfaceConstraint}, - CompileDefaultDomainDeformation(m_discretization.finiteElementModel()), - models::compileConstraint( - m_stellarModel.template specification(), - m_stellarModel.template specification() - ) + CompileDefaultDomainDeformation(m_discretization.finiteElementModel()) ) { VerifyProblem(); } @@ -107,14 +167,19 @@ export namespace mean_field::equilibrium { StellarEquilibriumProblem(StellarEquilibriumProblem &&) = delete; StellarEquilibriumProblem &operator=(StellarEquilibriumProblem &&) = delete; + public: [[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; } + [[nodiscard]] const NormalizationPrescriptionType &GetNormalizationPrescription() const noexcept { + return m_discretization.normalizationPrescription(); + } + [[nodiscard]] const CompiledSurfaceConstraintType &GetCompiledSurfaceConstraint() const noexcept { return m_compiledSurfaceConstraint; } @@ -127,6 +192,10 @@ export namespace mean_field::equilibrium { return m_preparedOperator; } + [[nodiscard]] const PhysicalCoreType &GetPhysicalOperator() const noexcept { + return m_preparedOperator.GetPhysicalOperator(); + } + [[nodiscard]] const auto &GetManifest() const noexcept { return m_preparedOperator.GetRootManifest(); } @@ -135,28 +204,20 @@ export namespace mean_field::equilibrium { return m_preparedOperator.IsPrepared(); } + [[nodiscard]] std::uint64_t GetPreparationGeneration() const noexcept { + return m_preparationGeneration; + } + [[nodiscard]] const operators::StellarEquilibriumDependencies &GetLinearizationDependencies() const { - if constexpr (hasFixedCentralDensity) { - return m_preparedOperator.GetPhysicalOperator().GetDependencies(); - } else { - return m_preparedOperator.GetDependencies(); - } + return GetPhysicalOperator().GetDependencies(); } [[nodiscard]] const operators::StellarEquilibriumDependencyStamp &GetGeometryDependency() const { - if constexpr (hasFixedCentralDensity) { - return m_preparedOperator.GetPhysicalOperator().GetGeneratedDisplacementDependency(); - } else { - return m_preparedOperator.GetGeneratedDisplacementDependency(); - } + return GetPhysicalOperator().GetGeneratedDisplacementDependency(); } [[nodiscard]] const field::FieldBoundaryDofMap &GetPressureSurfaceRows() const noexcept { - if constexpr (hasFixedCentralDensity) { - return m_preparedOperator.GetPhysicalOperator().GetSurfaceConstraintOperator().GetSurfaceRows(); - } else { - return m_preparedOperator.GetSurfaceConstraintOperator().GetSurfaceRows(); - } + return GetPhysicalOperator().GetSurfaceConstraintOperator().GetSurfaceRows(); } [[nodiscard]] int StateSize() const noexcept { @@ -175,8 +236,19 @@ export namespace mean_field::equilibrium { const mfem::Vector &state, const operators::StellarEquilibriumDependencies &dependencies, const physics::RigidRotation &rotation - ) { - return m_preparedOperator.Prepare(state, dependencies, rotation); + ) requires(generatedRotationProviderCount == 0) { + 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 { @@ -194,8 +266,8 @@ export namespace mean_field::equilibrium { [[nodiscard]] static CompiledSurfaceConstraintType CompileSurfaceConstraint(const ModelType &stellarModel) { return surface::compilePressureSurfaceConstraint< typename ThermodynamicEquationsType::PressureSurfaceFormulation>( - stellarModel.template specification(), - stellarModel.template specification() + stellarModel.surfaceCondition(), + stellarModel.equationOfState() ); } @@ -224,34 +296,112 @@ export namespace mean_field::equilibrium { MFEM_VERIFY(m_discretization.isCurrent(), "The stellar equilibrium problem has a stale discretization."); } - ModelType m_stellarModel; - StellarDiscretization m_discretization; + std::shared_ptr m_stellarModel; + DiscretizationType m_discretization; CompiledSurfaceConstraintType m_compiledSurfaceConstraint; PreparedOperatorType m_preparedOperator; + std::uint64_t m_preparationGeneration{0}; }; - template + template struct IsStellarEquilibriumProblem : std::false_type { }; + + template + requires detail::StellarEquilibriumModelDiscretizationStructureAudit< + std::remove_cvref_t, + std::remove_cvref_t>::value + struct IsStellarEquilibriumProblem> : std::true_type { }; + + template + concept DiscretizedStellarEquilibriumProblem = IsStellarEquilibriumProblem>::value; + + namespace detail { + template < + typename Model, + typename Discretization, + bool StructurallyCompatible = + StellarEquilibriumModelDiscretizationStructureAudit< + std::remove_cvref_t, + std::remove_cvref_t>::value> + struct StellarEquilibriumModelDiscretizationOperationAudit : std::false_type { }; + + template + struct StellarEquilibriumModelDiscretizationOperationAudit< + Model, + Discretization, + true> { + private: + using ModelType = std::remove_cvref_t; + using DiscretizationType = std::remove_cvref_t; + using Problem = StellarEquilibriumProblem; + using Prescription = typename DiscretizationType::NormalizationPrescriptionType; + + public: + static constexpr bool value = [] { + if constexpr ( + std::same_as || + normalization::PhysicalRieszDiagonalPrescription) { + return true; + } else { + return normalization::RuntimePreparedNormalizationOperation; + } + }(); + }; + } // 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 + concept StellarEquilibriumModelDiscretizationCompatible = + detail::StellarEquilibriumModelDiscretizationOperationAudit< + std::remove_cvref_t, + std::remove_cvref_t>::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 + requires StellarEquilibriumModelDiscretizationCompatible + [[nodiscard]] static auto Create( + Model &&stellarModel, + Discretization discretization + ) { + using ModelType = std::remove_cvref_t; + using DiscretizationType = std::remove_cvref_t; + return StellarEquilibriumProblem{ + std::forward(stellarModel), + std::move(discretization) + }; + } + }; + } // namespace detail + + template + requires StellarEquilibriumModelDiscretizationCompatible [[nodiscard]] auto discretize( Model &&stellarModel, - const StellarDiscretization discretization + Discretization discretization ) { - using ModelType = std::remove_cvref_t; - return StellarEquilibriumProblem{std::forward(stellarModel), discretization}; + return detail::StellarEquilibriumProblemFactory::Create( + std::forward(stellarModel), + std::move(discretization) + ); } template + requires StellarEquilibriumModelDiscretizationCompatible [[nodiscard]] auto discretize( Model &&stellarModel, fem::FEM &finiteElementModel ) { return discretize(std::forward(stellarModel), StellarDiscretization{finiteElementModel}); } - - template struct IsStellarEquilibriumProblem : std::false_type { }; - - template - struct IsStellarEquilibriumProblem> : std::true_type { }; - - template - concept DiscretizedStellarEquilibriumProblem = IsStellarEquilibriumProblem>::value; } // namespace mean_field::equilibrium diff --git a/libmeanfield/interface/preconditioning/equilibrium_coordinates.cppm b/libmeanfield/interface/preconditioning/equilibrium_coordinates.cppm index 2fabd7b..68687a3 100644 --- a/libmeanfield/interface/preconditioning/equilibrium_coordinates.cppm +++ b/libmeanfield/interface/preconditioning/equilibrium_coordinates.cppm @@ -292,7 +292,8 @@ export namespace mean_field::preconditioning { }; template - requires EquilibriumCoordinateComponentFor::FormType> + requires EquilibriumCoordinateComponentFor::FormType> && + SpecificationBorderPreparableFor class PreparedStellarPreconditioner final : public mfem::Solver { private: using ProblemType = std::remove_cvref_t; @@ -324,6 +325,9 @@ export namespace mean_field::preconditioning { } } + PreparedStellarPreconditioner(ProblemType &&, BlockType) = delete; + PreparedStellarPreconditioner(const ProblemType &&, BlockType) = delete; + PreparedStellarPreconditioner(const PreparedStellarPreconditioner &) = delete; PreparedStellarPreconditioner &operator=(const PreparedStellarPreconditioner &) = delete; PreparedStellarPreconditioner(PreparedStellarPreconditioner &&) = delete; @@ -367,6 +371,10 @@ export namespace mean_field::preconditioning { return m_grouped.GetBlock(); } + [[nodiscard]] const ProblemType &GetProblem() const noexcept { + return m_grouped.GetProblem(); + } + [[nodiscard]] const GroupedPreconditioner &GetGroupedPreconditioner() const noexcept { return m_grouped; } @@ -392,11 +400,26 @@ export namespace mean_field::preconditioning { SpecificationBorderBlockType Block> requires EquilibriumCoordinateComponentFor< Block, - typename std::remove_cvref_t::FormType> + typename std::remove_cvref_t::FormType> && + SpecificationBorderPreparableFor [[nodiscard]] auto prepare( const Problem &problem, Block block ) { return PreparedStellarPreconditioner{problem, std::move(block)}; } + + template + requires (!std::is_lvalue_reference_v) && + equilibrium::DiscretizedStellarEquilibriumProblem> && + EquilibriumCoordinateComponentFor< + Block, + typename std::remove_cvref_t::FormType> && + SpecificationBorderPreparableFor, Block> + [[nodiscard]] auto prepare( + Problem &&, + Block + ) -> PreparedStellarPreconditioner< + std::remove_cvref_t, + std::remove_cvref_t> = delete; } // namespace mean_field::preconditioning diff --git a/libmeanfield/interface/preconditioning/material_surface.cppm b/libmeanfield/interface/preconditioning/material_surface.cppm index 509c885..9423917 100644 --- a/libmeanfield/interface/preconditioning/material_surface.cppm +++ b/libmeanfield/interface/preconditioning/material_surface.cppm @@ -231,10 +231,90 @@ export namespace mean_field::preconditioning { template concept MaterialSurfaceDescriptor = detail::IsMaterialSurfaceDescriptor>::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 + struct MaterialSurfaceEquationOfStateBackend { + static constexpr bool registered = false; + }; + + template <> + struct MaterialSurfaceEquationOfStateBackend { + static constexpr bool registered = true; + using CoreType = operators::PreparedStellarEquilibriumOperator; + }; + + template + concept ImplementedMaterialSurfaceEquationOfState = requires { + { + MaterialSurfaceEquationOfStateBackend>::registered + } -> std::convertible_to; + requires MaterialSurfaceEquationOfStateBackend< + std::remove_cvref_t>::registered; + typename MaterialSurfaceEquationOfStateBackend>::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 + struct MaterialSurfaceExecutableRuntime { + static constexpr bool available = false; + }; + + template <> + struct MaterialSurfaceExecutableRuntime { + static constexpr bool available = true; + }; + + template + concept ExecutableMaterialSurfaceRuntimeFor = requires { + { + MaterialSurfaceExecutableRuntime< + std::remove_cvref_t, + std::remove_cvref_t>::available + } -> std::convertible_to; + requires MaterialSurfaceExecutableRuntime< + std::remove_cvref_t, + std::remove_cvref_t>::available; + }; + template concept ImplementedMaterialSurfaceDescriptor = MaterialSurfaceDescriptor && - std::same_as; + ImplementedMaterialSurfaceEquationOfState< + typename Descriptor::ThermodynamicEquations::EquationOfStateType>; + + template + concept MaterialSurfaceRuntimeFor = + ImplementedMaterialSurfaceDescriptor && requires { + typename MaterialSurfaceEquationOfStateBackend< + typename std::remove_cvref_t::ThermodynamicEquations::EquationOfStateType>::CoreType; + requires std::same_as< + std::remove_cvref_t, + typename MaterialSurfaceEquationOfStateBackend< + typename std::remove_cvref_t::ThermodynamicEquations::EquationOfStateType>::CoreType>; + requires ExecutableMaterialSurfaceRuntimeFor< + typename std::remove_cvref_t::ThermodynamicEquations::EquationOfStateType, + PhysicalCore>; + }; + + template + concept MaterialSurfacePreconditionerProblem = + equilibrium::DiscretizedStellarEquilibriumProblem && requires { + requires MaterialSurfaceRuntimeFor< + MaterialSurfaceDescriptorFor>, + typename std::remove_cvref_t::PhysicalCoreType>; + }; using DensityMassDiagonalCharacteristics = OperatorCharacteristics< OperatorCategory::mass_like, @@ -605,7 +685,7 @@ export namespace mean_field::preconditioning { }; template < - equilibrium::DiscretizedStellarEquilibriumProblem Problem, + MaterialSurfacePreconditionerProblem Problem, backend::Registered MaterialBackend = backend::Diagonal, backend::Registered SurfaceBackend = backend::Diagonal, MaterialSurfaceFactorizationPolicy Policy = SurfaceThenMaterialTriangular> @@ -623,7 +703,7 @@ export namespace mean_field::preconditioning { } template < - equilibrium::DiscretizedStellarEquilibriumProblem Problem, + MaterialSurfacePreconditionerProblem Problem, backend::Registered MaterialBackend, backend::Registered SurfaceBackend, MaterialSurfaceFactorizationPolicy Policy, @@ -688,7 +768,7 @@ export namespace mean_field::preconditioning { // direct coupling actions and does not pay for a full Jacobian // application. 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 fullSurfaceDirection = 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); const auto fullActionView = m_operation->GetRootManifest().residualView(m_fullAction); - const mfem::Vector fullDensityAction = fullActionView.block(utils::blocks::density_field.mass_term); - const mfem::Vector fullSurfaceAction = + const auto fullDensityAction = fullActionView.block(utils::blocks::density_field.mass_term); + const auto fullSurfaceAction = 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; surfaceAction = fullSurfaceAction; enthalpyAction = fullEnthalpyAction; @@ -1108,7 +1188,8 @@ export namespace mean_field::preconditioning { std::uint64_t surfaceH1Assemblies{0}; }; - template + template + requires MaterialSurfaceRuntimeFor class PreparedMaterialSurfaceBlock final : public mfem::Solver { public: using Block = MaterialSurfaceBlock; @@ -1562,9 +1643,10 @@ export namespace mean_field::preconditioning { }; template < - ImplementedMaterialSurfaceDescriptor Descriptor, + MaterialSurfaceDescriptor Descriptor, MaterialSurfaceFactorizationPolicy Policy, backend::ApplicationMode Mode> + requires MaterialSurfaceRuntimeFor class PreparedH1MaterialSurfaceBlock final : public mfem::Solver { public: using SurfaceBackend = backend::HypreBoomerAMG; @@ -2027,8 +2109,9 @@ export namespace mean_field::preconditioning { }; template < - ImplementedMaterialSurfaceDescriptor Descriptor, + MaterialSurfaceDescriptor Descriptor, MaterialSurfaceFactorizationPolicy Policy> + requires MaterialSurfaceRuntimeFor [[nodiscard]] auto prepare( const operators::PreparedStellarEquilibriumOperator &operation, MaterialSurfaceBlock< @@ -2042,26 +2125,26 @@ export namespace mean_field::preconditioning { template < equilibrium::StellarEquilibriumModel Model, + equilibrium::StellarDiscretizationType Discretization, MaterialSurfaceFactorizationPolicy Policy> + requires MaterialSurfacePreconditionerProblem< + equilibrium::StellarEquilibriumProblem> [[nodiscard]] auto prepare( - const equilibrium::StellarEquilibriumProblem &problem, + const equilibrium::StellarEquilibriumProblem &problem, MaterialSurfaceBlock< - MaterialSurfaceDescriptorFor>, + MaterialSurfaceDescriptorFor>, backend::Diagonal, backend::Diagonal, Policy> block ) { - if constexpr (equilibrium::StellarEquilibriumProblem::hasFixedCentralDensity) { - return prepare(problem.GetPreparedOperator().GetPhysicalOperator(), std::move(block)); - } else { - return prepare(problem.GetPreparedOperator(), std::move(block)); - } + return prepare(problem.GetPhysicalOperator(), std::move(block)); } template < - ImplementedMaterialSurfaceDescriptor Descriptor, + MaterialSurfaceDescriptor Descriptor, MaterialSurfaceFactorizationPolicy Policy, backend::ApplicationMode Mode> + requires MaterialSurfaceRuntimeFor [[nodiscard]] auto prepare( const operators::PreparedStellarEquilibriumOperator &operation, MaterialSurfaceBlock< @@ -2076,21 +2159,20 @@ export namespace mean_field::preconditioning { template < equilibrium::StellarEquilibriumModel Model, + equilibrium::StellarDiscretizationType Discretization, MaterialSurfaceFactorizationPolicy Policy, backend::ApplicationMode Mode> + requires MaterialSurfacePreconditionerProblem< + equilibrium::StellarEquilibriumProblem> [[nodiscard]] auto prepare( - const equilibrium::StellarEquilibriumProblem &problem, + const equilibrium::StellarEquilibriumProblem &problem, MaterialSurfaceBlock< - MaterialSurfaceDescriptorFor>, + MaterialSurfaceDescriptorFor>, backend::Diagonal, backend::HypreBoomerAMG, Policy, SurfaceH1MassStiffness> block ) { - if constexpr (equilibrium::StellarEquilibriumProblem::hasFixedCentralDensity) { - return prepare(problem.GetPreparedOperator().GetPhysicalOperator(), std::move(block)); - } else { - return prepare(problem.GetPreparedOperator(), std::move(block)); - } + return prepare(problem.GetPhysicalOperator(), std::move(block)); } } // namespace mean_field::preconditioning diff --git a/libmeanfield/interface/preconditioning/specification_border.cppm b/libmeanfield/interface/preconditioning/specification_border.cppm index c8c6ef0..8b9e09f 100644 --- a/libmeanfield/interface/preconditioning/specification_border.cppm +++ b/libmeanfield/interface/preconditioning/specification_border.cppm @@ -5,6 +5,7 @@ module; #include #include #include +#include #include #include #include @@ -15,43 +16,63 @@ module; export module mean_field:preconditioning.specification_border; +export import :operators.stellar_equilibrium_compiler; export import :preconditioning.stellar_equilibrium; export import :preconditioning.stellar_structure; export namespace mean_field::preconditioning { - template struct SpecificationBorderContribution { - using CorrectionBlocks = utils::blocks::type_list<>; - using ResidualBlocks = utils::blocks::type_list<>; - using RequiredCouplings = utils::blocks::type_list<>; + namespace detail { + template struct ToPreconditionerCouplings; - static constexpr bool registered = false; - }; + template + struct ToPreconditionerCouplings> { + using Type = utils::blocks::type_list...>; + }; - template <> struct SpecificationBorderContribution { - using LayoutRequest = models::FixedMassLayoutRequest; - using CorrectionBlock = typename LayoutRequest::ValueBlockType; - using ResidualBlock = typename LayoutRequest::ResidualBlockType; - using CorrectionBlocks = utils::blocks::type_list; - using ResidualBlocks = utils::blocks::type_list; - using RequiredCouplings = utils::blocks::type_list< - Coupling, - Coupling, - Coupling>; + template + struct CollectUniqueCouplings; - static constexpr bool registered = true; - }; + template + struct CollectUniqueCouplings, Accumulated> { + using Type = Accumulated; + }; - template <> struct SpecificationBorderContribution { - using LayoutRequest = models::CentralDensityLayoutRequest; - using CorrectionBlock = typename LayoutRequest::ValueBlockType; - using ResidualBlock = typename LayoutRequest::ResidualBlockType; - using CorrectionBlocks = utils::blocks::type_list; - using ResidualBlocks = utils::blocks::type_list; - using RequiredCouplings = utils::blocks::type_list< - Coupling, - Coupling>; + template + struct CollectUniqueCouplings< + utils::blocks::type_list, + Accumulated> { + using Type = typename CollectUniqueCouplings< + utils::blocks::type_list, + AppendUniqueT>::Type; + }; - static constexpr bool registered = true; + template + using UniqueConcatenatedCouplingsT = typename CollectUniqueCouplings< + ConcatenateT, + utils::blocks::type_list<>>::Type; + } // namespace detail + + /* + * Preconditioner topology is a projection of the authoritative operator + * compilation. It therefore cannot silently drift from the residual or + * Jacobian when a new specification is added. + */ + template + struct SpecificationBorderContribution { + private: + using OperatorCompilation = + operators::StellarEquilibriumSpecificationCompilation; + + public: + using CorrectionBlocks = typename OperatorCompilation::GeneratedCorrectionBlocks; + using ResidualBlocks = typename OperatorCompilation::GeneratedResidualBlocks; + using RequiredCouplings = typename detail::ToPreconditionerCouplings< + typename OperatorCompilation::IncidentJacobianCouplings>::Type; + + static constexpr bool registered = + operators::stellarEquilibriumSpecificationCompilationComplete; }; namespace detail { @@ -90,7 +111,7 @@ export namespace mean_field::preconditioning { typename SpecificationBorderContribution::CorrectionBlocks...>; using ResidualBlocks = preconditioning::detail::ConcatenateT< typename SpecificationBorderContribution::ResidualBlocks...>; - using RequiredCouplings = preconditioning::detail::ConcatenateT< + using RequiredCouplings = preconditioning::detail::UniqueConcatenatedCouplingsT< typename SpecificationBorderContribution::RequiredCouplings...>; static constexpr std::size_t valueArity = @@ -98,7 +119,7 @@ export namespace mean_field::preconditioning { static constexpr std::size_t residualArity = (std::size_t{0} + ... + generatedBorderResidualArity); static constexpr std::size_t specificationCount = - (std::size_t{0} + ... + (SpecificationBorderContribution::registered ? 1U : 0U)); + (std::size_t{0} + ... + (specificationGeneratesBorder ? 1U : 0U)); static constexpr bool symbolicallySquare = valueArity == residualArity; }; @@ -228,8 +249,9 @@ export namespace mean_field::preconditioning { ConcatenateT; using ResidualBlocks = preconditioning::detail:: ConcatenateT; - using RequiredCouplings = preconditioning::detail:: - ConcatenateT; + using RequiredCouplings = preconditioning::detail::UniqueConcatenatedCouplingsT< + typename StructureComponent::RequiredCouplings, + typename CompiledBorder::RequiredCouplings>; using OperatorDescription = BorderedStellarStructureCharacteristics; using BackendType = backend::BorderedStellarStructure; @@ -290,24 +312,1270 @@ export namespace mean_field::preconditioning { }; namespace detail { - template - [[nodiscard]] const operators::PreparedStellarEquilibriumOperator & - specificationBorderPhysicalOperator(const Problem &problem) { - if constexpr (std::remove_cvref_t::hasFixedCentralDensity) { - return problem.GetPreparedOperator().GetPhysicalOperator(); + template struct SingleSpecificationBorderBlock; + + template + struct SingleSpecificationBorderBlock> { + using Type = Block; + }; + + template + using GeneratedSpecificationValueBlock = typename SingleSpecificationBorderBlock< + typename SpecificationBorderContribution::CorrectionBlocks>::Type; + + template + using GeneratedSpecificationResidualBlock = typename SingleSpecificationBorderBlock< + typename SpecificationBorderContribution::ResidualBlocks>::Type; + + template struct GeneratedBorderValueOwner { + static constexpr bool available = false; + }; + + template + requires requires { typename Generated::SpecificationType; } + struct GeneratedBorderValueOwner> { + using Specification = typename Generated::SpecificationType; + + static constexpr bool available = models::ModelSpecification; + }; + + template struct GeneratedBorderResidualOwner { + static constexpr bool available = false; + }; + + template + requires requires { typename Generated::SpecificationType; } + struct GeneratedBorderResidualOwner> { + using Specification = typename Generated::SpecificationType; + + static constexpr bool available = models::ModelSpecification; + }; + + template + inline constexpr bool isGeneratedBorderValue = + GeneratedBorderValueOwner>::available; + + template + inline constexpr bool isGeneratedBorderResidual = + GeneratedBorderResidualOwner>::available; + + template + inline constexpr bool isStellarStructureValue = + std::same_as, utils::blocks::density::mass::value> || + std::same_as, utils::blocks::surface_deformation::parameters::value> || + std::same_as, utils::blocks::enthalpy::specific::value> || + std::same_as, utils::blocks::gravity::gradient::value> || + std::same_as, utils::blocks::gravity::poisson::value>; + + template + inline constexpr bool isStellarStructureResidual = + std::same_as, utils::blocks::density::mass::residual> || + std::same_as< + std::remove_cvref_t, + utils::blocks::surface_deformation::shape_equilibrium::residual> || + std::same_as, utils::blocks::enthalpy::specific::residual> || + std::same_as, utils::blocks::gravity::gradient::residual> || + std::same_as, utils::blocks::gravity::poisson::residual>; + + enum class SpecificationBorderOperation { + structure_to_border, + border_to_structure, + border_to_border + }; + + template + [[nodiscard]] consteval bool directionParticipatesInCoupling() { + if constexpr (!std::same_as, typename Coupling::Correction>) { + return false; + } else if constexpr (Operation == SpecificationBorderOperation::structure_to_border) { + return isStellarStructureValue && isGeneratedBorderResidual; + } else if constexpr (Operation == SpecificationBorderOperation::border_to_structure) { + return isGeneratedBorderValue && isStellarStructureResidual; } else { - return problem.GetPreparedOperator(); + return isGeneratedBorderValue && isGeneratedBorderResidual; } } - template - class PreparedSpecificationBorderAction { - static_assert( - !specificationGeneratesBorder, - "A generated model specification requires a prepared specification-border action specialization." + template + [[nodiscard]] consteval bool actionParticipatesInCoupling() { + if constexpr (!std::same_as, typename Coupling::Residual>) { + return false; + } else if constexpr (Operation == SpecificationBorderOperation::structure_to_border) { + return isGeneratedBorderResidual && isStellarStructureValue; + } else if constexpr (Operation == SpecificationBorderOperation::border_to_structure) { + return isStellarStructureResidual && isGeneratedBorderValue; + } else { + return isGeneratedBorderResidual && isGeneratedBorderValue; + } + } + + template + [[nodiscard]] const mfem::Vector &structureDirectionBlock( + const StellarStructureDirectionView &view + ) { + using BlockType = std::remove_cvref_t; + static_assert(isStellarStructureValue); + if constexpr (std::same_as) { + return view.density; + } else if constexpr ( + std::same_as) { + return view.surface; + } else if constexpr (std::same_as) { + return view.enthalpy; + } else if constexpr (std::same_as) { + return view.gravityGradient; + } else { + return view.gravityPotential; + } + } + + template + [[nodiscard]] mfem::Vector &structureActionBlock(StellarStructureActionView &view) { + using BlockType = std::remove_cvref_t; + static_assert(isStellarStructureResidual); + if constexpr (std::same_as) { + return view.density; + } else if constexpr ( + std::same_as) { + return view.surface; + } else if constexpr (std::same_as) { + return view.enthalpy; + } else if constexpr (std::same_as) { + return view.gravityGradient; + } else { + return view.gravityPotential; + } + } + + template + concept SpecificationBelongsToProblem = + models::ModelSpecification> && + equilibrium::DiscretizedStellarEquilibriumProblem> && + std::remove_cvref_t::ModelType::template containsSpecification< + std::remove_cvref_t>; + } // namespace detail + + /* A single callback sees one compiled Jacobian edge, never the union of all + * legal sources and rows for its specification. Binding both block types + * into these two tiny views prevents a callback from reading one direction + * block while claiming that its contribution differentiates another. */ + template < + models::ModelSpecification Specification, + equilibrium::DiscretizedStellarEquilibriumProblem Problem, + detail::SpecificationBorderOperation Operation, + typename ResidualBlock, + typename ValueBlock> + requires detail::SpecificationBelongsToProblem + class SpecificationBorderCouplingDirectionView final { + private: + using ProblemType = std::remove_cvref_t; + using Model = typename ProblemType::ModelType; + using CouplingType = Coupling< + std::remove_cvref_t, + std::remove_cvref_t>; + using Couplings = + typename SpecificationBorderContribution::RequiredCouplings; + + static constexpr bool permitted = + utils::blocks::contains_type_v< + CouplingType, + Couplings> && + detail::directionParticipatesInCoupling< + std::remove_cvref_t, + CouplingType, + Operation>() && + detail::actionParticipatesInCoupling< + std::remove_cvref_t, + CouplingType, + Operation>(); + + public: + explicit SpecificationBorderCouplingDirectionView( + const StellarStructureDirectionView &structure + ) noexcept + requires( + permitted && + Operation == detail::SpecificationBorderOperation::structure_to_border + ) + : m_structure(std::addressof(structure)) { + } + + explicit SpecificationBorderCouplingDirectionView( + const mfem::Vector &border + ) noexcept + requires( + permitted && + Operation != detail::SpecificationBorderOperation::structure_to_border + ) + : m_border(std::addressof(border)) { + } + + [[nodiscard]] decltype(auto) values() const { + if constexpr (detail::isStellarStructureValue) { + return detail::structureDirectionBlock(*m_structure); + } else { + using Owner = + typename detail::GeneratedBorderValueOwner::Specification; + constexpr int offset = static_cast( + specificationBorderValueOffset + ); + return operators::ReadOnlyVectorView{ + *m_border, + offset, + ValueBlock::static_block_size + }; + } + } + + template + requires requires { typename std::remove_cvref_t::value; } && + std::same_as< + ValueBlock, + typename std::remove_cvref_t::value> + [[nodiscard]] decltype(auto) block(const Term &) const { + return values(); + } + + [[nodiscard]] int Size() const { + return values().Size(); + } + + [[nodiscard]] int size() const { + return Size(); + } + + [[nodiscard]] double operator()(const int index) const { + return values()(index); + } + + [[nodiscard]] decltype(auto) density() const + requires std::same_as { + return values(); + } + + [[nodiscard]] decltype(auto) surfaceShape() const + requires std::same_as< + ValueBlock, + utils::blocks::surface_deformation::parameters::value> { + return values(); + } + + [[nodiscard]] decltype(auto) specificEnthalpy() const + requires std::same_as { + return values(); + } + + [[nodiscard]] decltype(auto) gravityGradient() const + requires std::same_as { + return values(); + } + + [[nodiscard]] decltype(auto) gravitationalPotential() const + requires std::same_as { + return values(); + } + + [[nodiscard]] decltype(auto) gravityPotential() const + requires std::same_as { + return gravitationalPotential(); + } + + template + requires std::same_as< + ValueBlock, + detail::GeneratedSpecificationValueBlock> + [[nodiscard]] decltype(auto) generatedCoordinate() const { + return values(); + } + + private: + const StellarStructureDirectionView *m_structure{nullptr}; + const mfem::Vector *m_border{nullptr}; + }; + + template < + models::ModelSpecification Specification, + equilibrium::DiscretizedStellarEquilibriumProblem Problem, + detail::SpecificationBorderOperation Operation, + typename ResidualBlock, + typename ValueBlock> + requires detail::SpecificationBelongsToProblem + class SpecificationBorderCouplingRowAction final { + private: + using ProblemType = std::remove_cvref_t; + using Model = typename ProblemType::ModelType; + using CouplingType = Coupling< + std::remove_cvref_t, + std::remove_cvref_t>; + using Couplings = + typename SpecificationBorderContribution::RequiredCouplings; + + static constexpr bool permitted = + utils::blocks::contains_type_v && + detail::directionParticipatesInCoupling< + std::remove_cvref_t, + CouplingType, + Operation>() && + detail::actionParticipatesInCoupling< + std::remove_cvref_t, + CouplingType, + Operation>(); + + public: + SpecificationBorderCouplingRowAction( + StellarStructureActionView &structure, + const field::FieldBoundaryDofMap &surfaceRows + ) noexcept + requires( + permitted && + Operation == detail::SpecificationBorderOperation::border_to_structure + ) + : m_structure(std::addressof(structure)), + m_surfaceRows(std::addressof(surfaceRows)) { + } + + explicit SpecificationBorderCouplingRowAction(mfem::Vector &border) noexcept + requires( + permitted && + Operation != detail::SpecificationBorderOperation::border_to_structure + ) + : m_border(std::addressof(border)) { + } + + [[nodiscard]] stellar::ContributionAdded add( + const double contribution + ) { + RequireUnused(); + decltype(auto) target = block(); + target += contribution; + RestoreReplacedRows(target, contribution); + SynchronizeGeneratedBlock(target); + m_addCount = 1; + return {}; + } + + [[nodiscard]] stellar::ContributionAdded add( + const mfem::Vector &contribution + ) { + RequireUnused(); + decltype(auto) target = block(); + if (target.Size() != contribution.Size()) { + throw std::invalid_argument( + "A specification-border physics contribution has the wrong block size." + ); + } + target += contribution; + RestoreReplacedRows(target, contribution); + SynchronizeGeneratedBlock(target); + m_addCount = 1; + return {}; + } + + template + void Verify(const Result &) const { + if constexpr (std::same_as< + std::remove_cvref_t, + stellar::ContributionAdded>) { + if (m_addCount != 1) { + throw std::logic_error( + "A specification-border provider returned ContributionAdded without adding exactly once." + ); + } + } else if (m_addCount != 0) { + throw std::logic_error( + "A specification-border provider returned StructuralZero after adding to its row." + ); + } + } + + private: + void RequireUnused() const { + if (m_addCount != 0) { + throw std::logic_error( + "A compiler-enumerated specification-border edge may be assembled only once." + ); + } + } + + [[nodiscard]] decltype(auto) block() const { + if constexpr (detail::isStellarStructureResidual) { + return detail::structureActionBlock(*m_structure); + } else { + using Owner = + typename detail::GeneratedBorderResidualOwner::Specification; + constexpr int offset = static_cast( + specificationBorderResidualOffset + ); + return mfem::Vector( + m_border->GetData() + offset, + ResidualBlock::static_block_size + ); + } + } + + void RestoreReplacedRows( + mfem::Vector &target, + const double contribution + ) const { + if constexpr ( + Operation == detail::SpecificationBorderOperation::border_to_structure && + std::same_as) { + for (const int row : m_surfaceRows->reduced_dofs()) { + target(row) -= contribution; + } + } + } + + void RestoreReplacedRows( + mfem::Vector &target, + const mfem::Vector &contribution + ) const { + if constexpr ( + Operation == detail::SpecificationBorderOperation::border_to_structure && + std::same_as) { + for (const int row : m_surfaceRows->reduced_dofs()) { + target(row) -= contribution(row); + } + } + } + + void SynchronizeGeneratedBlock(mfem::Vector &target) const { + if constexpr (detail::isGeneratedBorderResidual) { + target.SyncAliasMemory(*m_border); + } + } + + StellarStructureActionView *m_structure{nullptr}; + mfem::Vector *m_border{nullptr}; + const field::FieldBoundaryDofMap *m_surfaceRows{nullptr}; + int m_addCount{0}; + }; + + /* This is the only object delivered to extension physics for an operation. + * A callback is invoked only after an exact compiled (row, source) edge has + * been selected. Its direction argument contains that source alone and + * its row argument supports additive updates to that row alone. */ + template < + models::ModelSpecification Specification, + equilibrium::DiscretizedStellarEquilibriumProblem Problem, + detail::SpecificationBorderOperation Operation> + requires detail::SpecificationBelongsToProblem + class SpecificationBorderActionView final { + private: + using Couplings = + typename SpecificationBorderContribution::RequiredCouplings; + + template + using Direction = SpecificationBorderCouplingDirectionView< + Specification, + Problem, + Operation, + std::remove_cvref_t, + std::remove_cvref_t>; + + template + using RowAction = SpecificationBorderCouplingRowAction< + Specification, + Problem, + Operation, + std::remove_cvref_t, + std::remove_cvref_t>; + + template + static constexpr bool permitsCoupling = + utils::blocks::contains_type_v< + Coupling< + std::remove_cvref_t, + std::remove_cvref_t>, + Couplings> && + detail::directionParticipatesInCoupling< + std::remove_cvref_t, + Coupling< + std::remove_cvref_t, + std::remove_cvref_t>, + Operation>() && + detail::actionParticipatesInCoupling< + std::remove_cvref_t, + Coupling< + std::remove_cvref_t, + std::remove_cvref_t>, + Operation>(); + + template + static constexpr bool completesCoupling = requires( + Callback &&callback, + const Direction &direction, + RowAction &row + ) { + { + std::forward(callback)(direction, row) + } -> stellar::ContributionResult; + }; + + public: + SpecificationBorderActionView( + const StellarStructureDirectionView &direction, + mfem::Vector &action + ) noexcept + requires(Operation == detail::SpecificationBorderOperation::structure_to_border) + : m_structureDirection(std::addressof(direction)), + m_borderAction(std::addressof(action)) { + } + + SpecificationBorderActionView( + const mfem::Vector &direction, + StellarStructureActionView &action, + const field::FieldBoundaryDofMap &surfaceRows + ) noexcept + requires(Operation == detail::SpecificationBorderOperation::border_to_structure) + : m_borderDirection(std::addressof(direction)), + m_structureAction(std::addressof(action)), + m_surfaceRows(std::addressof(surfaceRows)) { + } + + SpecificationBorderActionView( + const mfem::Vector &direction, + mfem::Vector &action + ) noexcept + requires(Operation == detail::SpecificationBorderOperation::border_to_border) + : m_borderDirection(std::addressof(direction)), + m_borderAction(std::addressof(action)) { + } + + template + requires requires { + typename std::remove_cvref_t::residual; + typename std::remove_cvref_t::value; + } && permitsCoupling< + typename std::remove_cvref_t::residual, + typename std::remove_cvref_t::value> && + completesCoupling< + typename std::remove_cvref_t::residual, + typename std::remove_cvref_t::value, + Callback> + void add( + const ResidualTerm &, + const ValueTerm &, + Callback &&callback + ) const { + using Residual = typename std::remove_cvref_t::residual; + using Value = typename std::remove_cvref_t::value; + const Direction direction = makeDirection(); + RowAction row = makeRowAction(); + decltype(auto) result = + std::forward(callback)(direction, row); + row.Verify(result); + } + + template + void addDensityFrom(const ValueTerm &valueTerm, Callback &&callback) const + requires requires { + typename std::remove_cvref_t::value; + } && permitsCoupling< + utils::blocks::density::mass::residual, + typename std::remove_cvref_t::value> && + completesCoupling< + utils::blocks::density::mass::residual, + typename std::remove_cvref_t::value, + Callback> { + add( + utils::blocks::density_field.mass_term, + valueTerm, + std::forward(callback) ); + } + + template + void addSurfaceShapeFrom(const ValueTerm &valueTerm, Callback &&callback) const + requires requires { + typename std::remove_cvref_t::value; + } && permitsCoupling< + utils::blocks::surface_deformation::shape_equilibrium::residual, + typename std::remove_cvref_t::value> && + completesCoupling< + utils::blocks::surface_deformation::shape_equilibrium::residual, + typename std::remove_cvref_t::value, + Callback> { + add( + utils::blocks::surface_deformation_field.shape_equilibrium_term, + valueTerm, + std::forward(callback) + ); + } + + template + void addSpecificEnthalpyFrom(const ValueTerm &valueTerm, Callback &&callback) const + requires requires { + typename std::remove_cvref_t::value; + } && permitsCoupling< + utils::blocks::enthalpy::specific::residual, + typename std::remove_cvref_t::value> && + completesCoupling< + utils::blocks::enthalpy::specific::residual, + typename std::remove_cvref_t::value, + Callback> { + add( + utils::blocks::enthalpy_field.specific_term, + valueTerm, + std::forward(callback) + ); + } + + template + void addGravityGradientFrom(const ValueTerm &valueTerm, Callback &&callback) const + requires requires { + typename std::remove_cvref_t::value; + } && permitsCoupling< + utils::blocks::gravity::gradient::residual, + typename std::remove_cvref_t::value> && + completesCoupling< + utils::blocks::gravity::gradient::residual, + typename std::remove_cvref_t::value, + Callback> { + add( + utils::blocks::gravity_field.gradient_term, + valueTerm, + std::forward(callback) + ); + } + + template + void addGravityPotentialFrom(const ValueTerm &valueTerm, Callback &&callback) const + requires requires { + typename std::remove_cvref_t::value; + } && permitsCoupling< + utils::blocks::gravity::poisson::residual, + typename std::remove_cvref_t::value> && + completesCoupling< + utils::blocks::gravity::poisson::residual, + typename std::remove_cvref_t::value, + Callback> { + add( + utils::blocks::gravity_field.poisson_term, + valueTerm, + std::forward(callback) + ); + } + + template + void addConstraintResidualFrom( + const ValueTerm &valueTerm, + Callback &&callback + ) const + requires requires { + typename std::remove_cvref_t::value; + } && permitsCoupling< + detail::GeneratedSpecificationResidualBlock, + typename std::remove_cvref_t::value> && + completesCoupling< + detail::GeneratedSpecificationResidualBlock, + typename std::remove_cvref_t::value, + Callback> { + struct GeneratedResidualTerm final { + using residual = detail::GeneratedSpecificationResidualBlock; + }; + add(GeneratedResidualTerm{}, valueTerm, std::forward(callback)); + } + + private: + template + [[nodiscard]] Direction makeDirection() const { + if constexpr (Operation == detail::SpecificationBorderOperation::structure_to_border) { + return Direction{*m_structureDirection}; + } else { + return Direction{*m_borderDirection}; + } + } + + template + [[nodiscard]] RowAction makeRowAction() const { + if constexpr (Operation == detail::SpecificationBorderOperation::border_to_structure) { + return RowAction{*m_structureAction, *m_surfaceRows}; + } else { + return RowAction{*m_borderAction}; + } + } + + const StellarStructureDirectionView *m_structureDirection{nullptr}; + const mfem::Vector *m_borderDirection{nullptr}; + StellarStructureActionView *m_structureAction{nullptr}; + mfem::Vector *m_borderAction{nullptr}; + const field::FieldBoundaryDofMap *m_surfaceRows{nullptr}; + }; + + template + using SpecificationStructureToBorderActionView = SpecificationBorderActionView< + Specification, + Problem, + detail::SpecificationBorderOperation::structure_to_border>; + + template + using SpecificationBorderToStructureActionView = SpecificationBorderActionView< + Specification, + Problem, + detail::SpecificationBorderOperation::border_to_structure>; + + template + using SpecificationBorderToBorderActionView = SpecificationBorderActionView< + Specification, + Problem, + detail::SpecificationBorderOperation::border_to_border>; + + /** The exact prepared equilibrium-physics object owned by one + * specification slot in a discretized problem. Physics-facing border + * actions may snapshot coefficients from this object, but do not receive + * the enclosing Problem or its backend facilities. */ + template + requires detail::SpecificationBelongsToProblem + using PreparedSpecificationEquilibriumPhysicsT = std::remove_cvref_t &>() + .GetPreparedOperator() + .template GetPreparedContribution>() + )>; + + namespace detail { + template + struct PhysicsFacingBorderResidualTerm final { + using residual = ResidualBlock; + }; + + template + struct PhysicsFacingBorderValueTerm final { + using value = ValueBlock; + }; + + template + struct PhysicsDerivativeTraits; + + template + struct PhysicsDerivativeTraits< + models::stellar::Derivative> final { + using EquationTag = Equation; + using StateTag = State; + }; + + template < + models::ModelSpecification Specification, + typename Problem, + SpecificationBorderOperation Operation, + typename Derivative> + struct SpecificationBorderDerivative final { + using Traits = PhysicsDerivativeTraits; + using ResidualBlock = typename operators::detail::StellarDependencyBlock< + Specification, + typename Traits::EquationTag>::Type; + using ValueBlock = typename operators::detail::StellarDependencyBlock< + Specification, + typename Traits::StateTag>::Type; + using CouplingType = Coupling; + using Direction = SpecificationBorderCouplingDirectionView< + Specification, + Problem, + Operation, + ResidualBlock, + ValueBlock>; + using Row = SpecificationBorderCouplingRowAction< + Specification, + Problem, + Operation, + ResidualBlock, + ValueBlock>; + + static constexpr bool participates = + utils::blocks::contains_type_v< + CouplingType, + typename SpecificationBorderContribution< + Specification>::RequiredCouplings> && + directionParticipatesInCoupling< + ValueBlock, + CouplingType, + Operation>() && + actionParticipatesInCoupling< + ResidualBlock, + CouplingType, + Operation>(); + }; + + template < + typename Physics, + models::ModelSpecification Specification, + typename Problem, + SpecificationBorderOperation Operation, + typename Derivatives> + struct ExactSpecificationBorderProviderSet; + + template < + typename Physics, + models::ModelSpecification Specification, + typename Problem, + SpecificationBorderOperation Operation, + typename... Derivatives> + struct ExactSpecificationBorderProviderSet< + Physics, + Specification, + Problem, + Operation, + utils::blocks::type_list> final { + private: + template + [[nodiscard]] static consteval bool ProviderIsComplete() { + using Edge = SpecificationBorderDerivative< + Specification, + Problem, + Operation, + Derivative>; + if constexpr (!Edge::participates) { + return true; + } else { + return requires( + const Physics &physics, + const typename Edge::Direction &direction, + typename Edge::Row &row + ) { + { + physics.ApplyJacobianAction( + Derivative{}, + direction, + row + ) + } -> stellar::ContributionResult; + }; + } + } public: + static constexpr bool complete = + (ProviderIsComplete() && ...); + + static void Apply( + const Physics &physics, + SpecificationBorderActionView< + Specification, + Problem, + Operation> action + ) requires complete { + (ApplyOne(physics, action), ...); + } + + private: + template + static void ApplyOne( + const Physics &physics, + const SpecificationBorderActionView< + Specification, + Problem, + Operation> &action + ) { + using Edge = SpecificationBorderDerivative< + Specification, + Problem, + Operation, + Derivative>; + if constexpr (Edge::participates) { + action.add( + PhysicsFacingBorderResidualTerm< + typename Edge::ResidualBlock>{}, + PhysicsFacingBorderValueTerm< + typename Edge::ValueBlock>{}, + [&](const auto &direction, auto &row) -> decltype(auto) { + return physics.ApplyJacobianAction( + Derivative{}, + direction, + row + ); + } + ); + } + } + }; + + template < + typename Physics, + models::ModelSpecification Specification, + equilibrium::DiscretizedStellarEquilibriumProblem Problem> + struct ExactSpecificationBorderProviders final { + using Model = typename Problem::ModelType; + using Derivatives = typename operators::StellarEquilibriumContributionTopology< + Specification, + Model>::Derivatives; + using StructureToBorder = ExactSpecificationBorderProviderSet< + Physics, + Specification, + Problem, + SpecificationBorderOperation::structure_to_border, + Derivatives>; + using BorderToStructure = ExactSpecificationBorderProviderSet< + Physics, + Specification, + Problem, + SpecificationBorderOperation::border_to_structure, + Derivatives>; + using BorderToBorder = ExactSpecificationBorderProviderSet< + Physics, + Specification, + Problem, + SpecificationBorderOperation::border_to_border, + Derivatives>; + + static constexpr bool complete = + StructureToBorder::complete && + BorderToStructure::complete && + BorderToBorder::complete; + }; + } // namespace detail + + /* + * Public protocol check for one prepared numerical constraint action. + * The topology itself is inferred from ModelDefinition; this concept makes + * the remaining physics implementation fail at the contribution boundary + * instead of deep inside the assembled preconditioner. + */ + template + concept PreparedSpecificationBorderActionFor = + std::constructible_from && + std::move_constructible && + requires( + const Candidate &action, + const StellarStructureDirectionView &structureDirection, + const mfem::Vector &borderDirection, + StellarStructureActionView structureAction, + mfem::Vector &borderAction + ) { + { Candidate::registered } -> std::convertible_to; + requires Candidate::registered; + action.ApplyStructureToBorder(structureDirection, borderAction); + action.ApplyBorderToStructure(borderDirection, structureAction); + action.ApplyBorderToBorder(borderDirection, borderAction); + }; + + /* + * Physics-facing opt-in for a new generated constraint. The specification + * may name this wrapper as + * + * using SpecificationBorderPhysics = + * preconditioning::SpecificationBorderPhysics; + * + * where MyPreparedAction implements one overload for each border-incident + * derivative inferred from Reads/Changes, for example + * + * auto ApplyJacobianAction( + * stellar::Derivative, + * const auto &direction, + * auto &row) const { + * return row.add(coefficient * direction.specificEnthalpy()(0)); + * } + * + * The adapter enumerates the exact inferred set. Missing overloads fail at + * compile time; identically absent terms return stellar::zeroDerivative. + * Its constructor receives only the exact specification's prepared + * equilibrium physics, never the full Problem. The wrapper owns the + * generic construction plumbing; extension authors neither specialize a + * detail:: class nor reproduce pack traversal. + * LocalSpecificationBorderPhysics below removes even the class-template + * spelling for the common concrete-class case. + */ + template + concept CompleteSpecificationBorderPhysicsProvider = + models::ModelSpecification> && + detail::SpecificationBelongsToProblem && + equilibrium::DiscretizedStellarEquilibriumProblem< + std::remove_cvref_t> && + detail::ExactSpecificationBorderProviders< + std::remove_cvref_t, + std::remove_cvref_t, + std::remove_cvref_t>::complete; + + template + concept PreparedSpecificationBorderPhysicsActionFor = + CompleteSpecificationBorderPhysicsProvider< + Candidate, + Specification, + Problem> && + std::constructible_from< + Candidate, + const PreparedSpecificationEquilibriumPhysicsT< + std::remove_cvref_t, + std::remove_cvref_t> &> && + std::move_constructible; + + template