From 76818f2f828ca12606a495a0b84f5d6ce4b78dc6 Mon Sep 17 00:00:00 2001 From: Emily Boudreaux Date: Sun, 6 Sep 2026 10:15:00 -0400 Subject: [PATCH] feat(libmeanfield): variadic refactor also added normaliztion operator --- CMakeLists.txt | 24 +- experiments/preconditioning_diagnostics.cpp | 3 +- extension_example/CMakeLists.txt | 50 + extension_example/README.md | 55 + extension_example/demo.cpp | 43 + extension_example/ideal_gas_radiation.cppm | 403 +++ .../manual/physics_developer_manual.pdf | Bin 0 -> 365924 bytes .../manual/physics_developer_manual.tex | 1058 ++++++ extension_example/rotating_stellar_model.cppm | 61 + .../tests/ideal_gas_radiation.cpp | 292 ++ .../tests/rotating_stellar_model.cpp | 67 + .../operators/prepared_angular_momentum.cpp | 590 +++ ...ed_central_density_stellar_equilibrium.cpp | 223 -- .../prepared_hydrostatic_equilibrium.cpp | 41 + .../prepared_stellar_equilibrium.cpp | 68 +- .../seed/stellar_equilibrium_projection.cpp | 26 +- .../equilibrium/stellar_discretization.cppm | 131 +- .../interface/field/field_registry.cppm | 22 + libmeanfield/interface/mean_field.cppm | 6 +- .../compiled_fixed_angular_momentum.cppm | 74 + .../interface/models/specifications.cppm | 1302 ++++++- .../interface/models/typed_stellar_model.cppm | 115 +- .../normalization/normalization.cppm | 6 + .../interface/normalization/operators.cppm | 621 ++++ .../normalization/physical_riesz.cppm | 728 ++++ .../interface/normalization/plan.cppm | 376 ++ .../normalization/stellar_equilibrium.cppm | 921 +++++ .../operators/prepared_angular_momentum.cppm | 196 + ...d_central_density_stellar_equilibrium.cppm | 117 - ...ared_hydrostatic_equilibrium_operator.cppm | 9 + .../prepared_stellar_equilibrium.cppm | 21 + ...prepared_variadic_stellar_equilibrium.cppm | 3163 +++++++++++++++++ .../interface/operators/root_manifest.cppm | 1279 +++++-- .../stellar_equilibrium_compiler.cppm | 815 +++++ .../stellar_equilibrium_problem.cppm | 330 +- .../equilibrium_coordinates.cppm | 27 +- .../preconditioning/material_surface.cppm | 132 +- .../preconditioning/specification_border.cppm | 1845 +++++++++- .../preconditioning/stellar_equilibrium.cppm | 307 +- .../preconditioning/stellar_structure.cppm | 96 +- .../seed/stellar_equilibrium_projection.cppm | 510 ++- libmeanfield/interface/utils/blocks.cppm | 16 + .../fixed_magnetic_specific_energy.cpp | 2182 ++++++++++++ tests/field/field_registry.cpp | 8 +- tests/models/model_specifications.cpp | 138 + .../models/physics_specification_frontend.cpp | 1389 ++++++++ tests/models/typed_stellar_model.cpp | 172 +- tests/mpi/distributed_execution.cpp | 812 ++++- tests/normalization/physical_riesz.cpp | 828 +++++ tests/normalization/plan.cpp | 228 ++ tests/normalization/stellar_equilibrium.cpp | 1187 +++++++ tests/operators/prepared_angular_momentum.cpp | 531 +++ ...ed_central_density_stellar_equilibrium.cpp | 47 +- tests/operators/root_manifest.cpp | 1228 ++++++- .../stellar_equilibrium_compiler.cpp | 2409 +++++++++++++ .../operators/stellar_equilibrium_system.cpp | 393 +- .../equilibrium_coordinates.cpp | 34 + tests/preconditioning/material_surface.cpp | 205 +- .../preconditioning/specification_border.cpp | 1428 +++++++- tests/preconditioning/stellar_equilibrium.cpp | 239 +- tests/preconditioning/stellar_structure.cpp | 29 +- tests/seed/stellar_equilibrium_projection.cpp | 194 +- tests/user-api/stellar_equilibrium.cpp | 63 + 63 files changed, 28794 insertions(+), 1119 deletions(-) create mode 100644 extension_example/CMakeLists.txt create mode 100644 extension_example/README.md create mode 100644 extension_example/demo.cpp create mode 100644 extension_example/ideal_gas_radiation.cppm create mode 100644 extension_example/manual/physics_developer_manual.pdf create mode 100644 extension_example/manual/physics_developer_manual.tex create mode 100644 extension_example/rotating_stellar_model.cppm create mode 100644 extension_example/tests/ideal_gas_radiation.cpp create mode 100644 extension_example/tests/rotating_stellar_model.cpp create mode 100644 libmeanfield/impl/operators/prepared_angular_momentum.cpp delete mode 100644 libmeanfield/impl/operators/prepared_central_density_stellar_equilibrium.cpp create mode 100644 libmeanfield/interface/models/compiled_fixed_angular_momentum.cppm create mode 100644 libmeanfield/interface/normalization/normalization.cppm create mode 100644 libmeanfield/interface/normalization/operators.cppm create mode 100644 libmeanfield/interface/normalization/physical_riesz.cppm create mode 100644 libmeanfield/interface/normalization/plan.cppm create mode 100644 libmeanfield/interface/normalization/stellar_equilibrium.cppm create mode 100644 libmeanfield/interface/operators/prepared_angular_momentum.cppm delete mode 100644 libmeanfield/interface/operators/prepared_central_density_stellar_equilibrium.cppm create mode 100644 libmeanfield/interface/operators/prepared_variadic_stellar_equilibrium.cppm create mode 100644 libmeanfield/interface/operators/stellar_equilibrium_compiler.cppm create mode 100644 tests/extensions/fixed_magnetic_specific_energy.cpp create mode 100644 tests/models/physics_specification_frontend.cpp create mode 100644 tests/normalization/physical_riesz.cpp create mode 100644 tests/normalization/plan.cpp create mode 100644 tests/normalization/stellar_equilibrium.cpp create mode 100644 tests/operators/prepared_angular_momentum.cpp create mode 100644 tests/operators/stellar_equilibrium_compiler.cpp 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 0000000000000000000000000000000000000000..bbc3081f650a132c427eb6d7ede8c1d198ddbf47 GIT binary patch literal 365924 zcma&NQ;aA{lj`)p^3?OI zswR;a7NudJWrHG_TN+-2Vqjy%r^mN5w1nd3hN6=;u{Cox$7iIcXT<;S3yMzE!rIxy z5uZ-f+Q8XF*u==r*aV7~7s|=m(Zs+8%6;QlLnCgB70EYGuRx5YPH{*I=`mI2N(6>F zu?idlZHK##abA*#I_v=HszKnP28vo86tseaw{~oEb=i>sOE9&Mi^~b18i*HHB z40}{V@*pxUBXXt~aI`Mo`|9G#&-cqoWm-dYn{-rz{`@uVf=5Lo;RK76i-u^c%MaDMCW#%eMEzh1(N08Iwlxp+9w*yfDn2`~-CH9EwSDI5lc?0y_XW9i06g zcMTxZnZOh)WDm@e-EP)PPyHJCLCdSr^VS6b}ovzLRz_lw!MraekzAEf)b&Qh6n^VzLc^EwCmzn>zDbneMoYyf`0Zae`7^&}*VtU&z=6c?Z$8H3W znq5f5%qA{1-fpuiNCTg3t$C3{u2AB$!E-6mtbLOqKXg|omFMA)G?`)#D$=o;XB=8O z#!^%ffa)(atU0?m8HE;IclR5dZ2k0`ElguLbm>y%Nb38PNR)Pr+K6mD=ZZ1jRK-Ql zv4j+a^~v5XY3Y{GLEM%s15 z!GyEH56-{m5BAQ|$hSUV39PcOgH40so?y1(WVd2M4`EZYbfwdKS%vyy;u|F%@OB-E zshGf3Ob{L;+;mqELs@_Lr}ia>@?xex9)ztjtJA5TT>wAIegNfg-<*+bH$X8q#^D;^ z%Of>4_ZM90mw42Tu_z4f$`Kq~UF*9QVMzzJy29w?gTK2RdY|ZOEHWvDGt-fjvlmFl z8T4kB)%#ya6rm#t_@%3IJY`OVVcg0=IZg}H(bCs5ZU(&IdPx`mj@gF&1Ll*$GpRZ{OlVDA}A;_g0U*~$M7(}IZHP|PWU9h8oPYVI9z z=FX3fM0m#fAG%#`gwk;DJTZ6!+JUBdoTeF;>2#fInom9`3jn=l68(1Rv4)|Lbt?{v zD=31o4Y=ErH|fV-SICPf*aAeL1GOQXia+_@STY6InGD1ij8f_-6lwLaVNbn|Phgc9 zDZ&{X5KUy2ViTtd^-Dn3n0H)yXKP z!idK)^t$Rxfb=E@^rj=Xj+MXlwhVj#?aWNNQ3qr!nNag;4mC?-n`kzGi_d`}bY&$c zBNo8+UtP-l{llS)FfC^e!;0%OOJ6g5OfvgKhN5PTG>y7+^$J*rL%>e+fah~8{AN$i zfh;W)fjwCYjV6nTWIv_y^(cDk9DbOL&d*xLhDg9I?*W5>b15#&xw%ixxhb%EZ{3=j zk(S9LaN@#m=0WM`;ES0^QKt+{)PphF-d&pLP7($NCK#=#@v_cWv@+W{2*pVn6$F}x zuov%hs(RROnPY)VrlznIb_g}}xQHwjF?_69LOuDbH~m;@QI1E|9T|{y&$Ag~1ZKjdU zXT_}cXPReJSI)gjFton5yz6R8@e?N&Zd5A1xyt0n1o33H(lJK!oeUlf3ixe8p8L7WF+P<)n+*KX1IRVr^jZSLoXvVZY?HP$jX6=!4$vE z_HMNa-S8={T^_Kz2Vj?WwxQ2B_4hi9_Q6zGr?wyXA6uE1pCA0MdV$qQwM%QAi(XR! zC)k$*41G29x=Q%LpNTiA!D~lg!w#YQ^RtiFpp?>3ly^{7+`JiL%X)ENg9L8%qdW95 z69~tc-%2>GFux7S4h+7Gb0Kxq6PA%?bc;fHzjMo959zznVZFB4f5YQ*cra`rOy%^_OTgbz#zH%errFon$R9ug=U^|_%RZ(X8s5tey*4!}Pp<3^ z0uB^yNGsQ?9)f{L7pgjcOY39@!?V`d=dVh}Ctxv(5A_$hh+57JC)b^*&Ih7cg|(`$ zK4&$KADtBw?-)|312H@2Po)|Qw@Ca8q@|@3UXX%DwdAp%MvoRY9r`XB$ecl(c2LA1 zv5BrK%}kFzU0AytE?oKEj%QcnLwbGzKF~+8TgRJCz&82;0yK92=KzGJ`JWQP2hH?t zp2zWOsM1bVlEBhBt&}BrccjAEkgI(rb`cGw>ZbP=MHkm1yZjh=TAw$s<0z1CV=`l# zl|-f{+N@tWDLJLRGCy8jBs}s^8@eJHuvr=Br$+n~#I%*gfmkaY3IGhF#X7N0!r*_;*``)1!z*K=719+7%1}1tj#c zo)jwbS5r1Q4tLCD{xU~^vHI_ZIaAJUJQ6c@|4J+@dK7PAbRMXRcp#PEJ zi#~c=;;df~oO$GIbp!?Z$<5*{YZ+IqdV}6dv0r=B2!?DN@{Xr<1y&mW5G8wU{^`*^ z)R7fV|M+5|2t!G@M%kvLrmEwG0s~$hTyVjFL5jCQY5AkO91k5-#& z$`Uj>DZT`S_No!yA5r?Y!F=c>hA6XDCNHBnV66)Wa1#rt$Kx=3FR5e6@)~M~2!(}J zPpaDl`W4*L78xjYh=7!h3Ep6YJpJ#}eVCG457viaMY{><>z}5%X!BM%NVsKqw!sQz5BYUNwVDZ7hx6(o5;mAYCOJwjJ`;46`v9itA-$`ja_bJ+7PVF> z{suQ4$*bACGtUX)%KJRqife5$CKtfPb{tJlBTp#@!pu;Gi2|ZT`jaVM7p5FMI&?qa zNdVB&|G>Hbi*m{y_9pmra)y>l&NfhVviJ=2^#2uw9G#r;*_oLB|DeRkLjS*|V6*1B z1ai1c2jE1*=%@esFPw`iE-~e zX-u@cB!mG}TR|AN%ps_Kkit?Du}T7DAe2z~&>|yw+YB`G=06q@3RT9KK^=SPEz*ue z>7n8`G)#XaGyqi1gA7EC#BVVCr7BUGaD{|=P|ch11d@>uYzqkX>(Jx@2Edxw*%y@Z zRf=j&LDn_M&j4bm$S0Ub+rdF_zs7(whxYJAq~b39YgoWR&2iZG{9|PW06E{FGA+bT z<%btJ@*U+#`$G0dVnEWS{E@T+SO*sH%E~2yfPd8K!L0tkq5!?ngVlj(3_(d89mIiU z1&ZTi8lWF$V?)9i0XZYpML`;is*gfR`2qcatVK@I78LmhR1NqCj(IS558}!B@R!nd z`@1I_66=##jXTpPHSbCH$)>&`(I-C}o2XBCdO}&R^G1V2)*F}V&f<#|;`RnG7fQF;jm7nhE_MvwZ#{rAwb_F^=a?0aDFCqwDrn|v$ zCm~Axsq@?`!nei~#%~SPK^X@`6xeU@-w+5g5+GeZQogI#2h#702z)&%F%~&v^1Kg> znO%QdT@FLV>NViAHch(rZl)fJyE@Gi_Oe#Z>i5|N1FW@?`4>`>z}+!p>4 zLs6>m!WxyZ_Ql&zeX=G@N^;MX=aI9SZ(1vL_gMU6hWIRTQ+-I({Ha^)t9DU8b=KP% zi_#=ILW=83@f!B9xn<~wu+BKfSbV5O>DN$u%j`0=;ZW#T_9!}>t}*_&Q2>840&J~&;)jB#^S52v)(^2`qp=baOd zGY-=SH~)hewg6ac$G7zy`CTnzz4q#(JPh9lTY&QxUr2~KV-#1htnqu9r z>;uk$06r0m<^W;Vje4H?kx9r((0}}&u=o_+~OAw zOGmf%@$Wchc({maoh7sF1aH!F&U}-RiRw325B$fG?gG?|$>|FnU$?YO(ss8Ty%pKT zKk2M15tX7>+Q;V`CJkRrgRGa#a3g)R^-ae1D8drf`W-93{0lIo<<-@!?ii1D&YYg)W(Ca_zu z@h%#c#S21KEk0G=E!& z%RZYJIV4kf{BGqdu0;%x`&CYG-0d(u#Hn6XP}ZA8r=|(n%A@IUC{40oRHE}dOLE}$ z{B(K&ul~>g+j;S4Y$4}B)jc*-44=Cd63U*-Y^kT%f8ze;X`xpXo|)r8iX z!_PkhCh4H6Smk^uox)8}@beNd)6CR~ua0FgWB6Xir)&7-6$9=>_kBvo*!vyHu7KJQ zIlkn^%${sY4cr|Jn}6+Zen-db>iA^FjMS=4UR$?s&&4u6W5?{yvQ}<;GHxbgc^wz; znk~LBS1zl$_B#Z$DC@+h5rwNCt~=6oUA;QCYYQxiv|C6FF615?T~@Mkx1_37muYG= zS6I3%XI4}ntxM+qcjJ}p#v!RpeIGq?+IeZ?@w4ge!G+nYSzX%bQV!LN{cG~&`t>20 zOjh(|JV_f(*R~>Up)%&^q=VknTOCas$aeOU=yGe8ixP4d0z9Y~LTEmkd=QdQgB-%m z0fpf#@OiBmA^6sZ(z-|N{8zqI%s^-#$ERg45+E;L7ezH(7Ujd4Je!vJRT>kTme|8a zU~_)~NG3{)+3iU-Qh0J;9@opCg_xl5@-JQ_HxCMXXkbJ<3{)h>G_;*DU!s{kQgEfv zLA*8^E+A0|4a_Z^mXjl|FaoHksiGl{u5X5rxo7tfcTgI{DOA)H#XQ&=*uB;mJ;20y zH%LTx{;UIv^}$4KvGjh^b=Y57_+k7wMb)rU%a5Y)zZ`A8?=KQKO7~YC1ThH>5II{D_=G8I$~qYo^@ivY)Y$*9Kf9i<|P}Epc`CYkvF_XtZOpulQ$l8ylALdZ$uM* z4yu56X6PHte5ck{SyP8KYU7|;-=Bg@i~JmW;j*y$+l?9q?%Q3wmo~g+{jh|R+!q{J zUmGs(_aU=bFCpN^%_6bQ=F)ef+o9tj#5|SvlnH);E*CS+u9Gz`(0*YPu+K5=m!wkt zFtr)$?(X+pe&Kp!3>1yzaN;pWKvGHxFg(!?E!wvU4vVKFHI9$bnXQ4k_!%9|OrMr3 zWZuIZe=8rpWlUiZtvykT)Ue$w2NAL0=I+~OWFmy->!8M}W1zY=3pQD27j-7iKya~u zG-r6(MReQpNb+%{4w9U*$U1@-N~J>|Mby9>8_e{%x?PROL`RJaa|l*ylaH7B-k9a4YQazNkp*8JMRr*}}6rtzL|Rkddo8%4|TF z6aEm-qDMC!r#cq8AuEwn%JIdrsYiU^yGh}F#4!tcp)~Nyb3d0co%w8sJL%-T`WE7` zfZYZFFC{z+Q%kD&hM;~!&>-AClZ1bnWMn}4csJDlH%dypqfJZ^AbG;j2P)GoA=pmL zu$U0~u`4m$U@rcQ>a&mA>I0*r{PoYTQ#N_D#B-t=wKpWa|Z-{_SxRZRd!FY&7BL9lUX=1W|_B<^1YWc5)~B?2kt!R39p=$D0+dT>tetDH$$P#p#D-p!`6u4eoN`1VXNoI=b zV=zM-`Yy@-IIeo~oSDY%e?&{!7_FY7rrwOwukl!26Ia;C!;Zxd5>E=cJG5j8m`t^m zUHlp3f!VF{-nVae0q|x5mh>AY(6qQ)J*g}f+pXUwY@#o}qX81TEz#mXo|}|nLZDbX zB@iO#y0s`ZN!cW0L$8*RRb9i@cf5Io;#9F9UBgIM((Zn^uo}|KWeE&C) zje@wl#dQZ?5KeZd=Xx(^BgDV>9Y$=T`|NM?YW1j|QlsQFoPlaBAtv$+_x>tK{AHI9 z6%mOF0GGBnfze@CPN60yp*}w=r{YB-=ffS$k)BO-_yf(QmEFZ)-3T<8F&c!zK{BL?8U_!Q1e4}@?yf650Jt?53x670a=jOGbdNh2 z9foUaLV|UC0W6$@NAKi`mn6grs!FP;NH&<{1i9Kn*I&v85fK&r5EgXhns~~@Vtq%% zWBS4a#tEm?nO97b@dJ>E(s(7I?r^7Ql3_?5w#};WBoz-0eL@hAZGX>X-p3Blm_OD8 z)3db+1Jk-4BbvS5F-6gZ+{Id7><%=>IoxsTSmMonn~lM?TVA7o`ns8^pbx(4m;1bC7gHFLgG!7v=&Grgnk=_#k4Tb^D= z73H@~FS3+#k+0Fq<#;|~IF_T#y6*mUOATkDi_bAhJ4GzTyzN&Gr6T4F#s1Cn^wF1$<}a8j!ES&!NH<&AU3D3%~2YozX^&+BK7MTQe8?qgXyz<&1sBuDhQXO&cqM`!j?`1gxQkl4o&p${wRf402wUJj+V*AQca-yz7=%2+3lGW;PohX$9s3 zE4xBz1~PmXOPUwtc>_7F`(R0cKUMy$QG%u0{V?)Hc56E{+5$i!#M3e{jNv#}z(AiT zzfb4a7lFiiIT#{+sSVUPAM_G@+lZ)GdfcEDNC7x=WpB8v!EE4NV=V$KZ9?0gL|yRp zG+pV!3yfb|9>~kF#ssg$_yXo;vS)Bzb9zJ#-TN~driGa^L8~d;4eKl6!!(iPIptB$ z$D#%DXFl^L@_JAgehWk!4=XIr)04UN^2BgA#}k*~M4YxVUzNbLMv7tjDSstSi4{wB zIZSl*88upF;!X$L@oR#6ZN+n0mnlAE*1j(lLtVQ6{?HCYBVVGNxb^9)hmk|=%<+`+ zUT(1bd3w>ct(aoB@?ln8U58#Ugu56iQEBD5oK->gYOx@+!o5!nkiE zJVQTgV%-#!vD=(T>Rr2{ux+i#^|p4FOS8)JY8zT)^eeb3kL7h}{ewM9d4x?@x#63I zUIX#1Ww22|PD4X}AU4Qh7nYkCxSzkQz6Ha+C42fv@J{c3aM`Y>#+O8yoLNgQ6NTnI zDk3_^tKY~d^!xG>pss5O{_?;T9`@_#qk!3&236DQ3uF4_uL7hvykx)VF!^3ZmC5FZCk=5VjCI3TVe{Pb zQ)*`op;7|+Pu3$XJD=4k0ngU#wTns}Q2~R-jZ5bQuy1E-+ESOt`uR(Y!(>5TRC*04 ziYcSg@ zBta6@suyosO28&MK!%x@uIvOFaRDu0w72Mkqil-`#&%1$KaVK3g7O7@oYO42z>eJo7N!N~DpOyn5OY-l^)@XmLK@F&yh@HJ9=%9n z(8|%nBVh22NT>vIO^))^by1={t68=Slh=h`axxCTWCg+OStsywq@=YzuqdK%ioIW7 z=Z*>Kp9QYW=G^R0?Sa#nT&k;N<{$r*P6Cb&GKficR$AtU5NSek_CHzI{vI3WQYfyL zWy8n4D^oIj&GrTtkZdI#JB>YAUSRyjO*G32gxt-;a6Eo$Wc?qygIoLiHA=;sVvBCb zy|*QZ@eA2LX6kM({QO(ImOv=ygCexF@N-az7CeX$XI*1{SL}4&qbH5obdRGTf%b?;XWP6yBgWW)Xxsk0 zNd534rJhZw>djUw$cq^7jZzcfA3B?kCzCXiN8?HDpx%TF3%5%hu1M-%OLuB!iK?R2 z?lRQ}FjZJfU+jGTmH8vf$+7NwK)uy!AQDLRGAO>oD!riW` zt*=$H)m1#|T9r{GB%(bsj)z-bE*E3K%)}#;t1FI%fdc^C=X^RonQ?AzT=#EIqgK_> z>2@SL4+$Czjyo$ib6TjCz0LMkC*0jqu6aRvk0ria#wM-(;uXPeZ(avL;B^vQneUZ; zJm?Pa>9P)g{DV?Y{G=?i@e+|rgbWV%rdN4u4bb5udEhfKS1HnEDvUC2{dzx+52lxg z@zAm7Lb!av3N?(EXsc;?zi@9D-CrhYjTc5b>1<7!l~frgX`&~$KcoC~gBP(X+(%K9 zD3KPiBI82}dJ90rwaxiq*y2UOr)j=lUO8YLCNzT|ez^?m7 z94}(U8{n@ASDxy zB|Lo6oL6lUQ6==4S)}4dp$e!oK#49LAOTE4`0`ZxHTaG#GMd^Cl~I~F5Q?11ID51Y zn&6NLP5G>qY9Nn}V}M4x1`E+A+*o3yn6PVxU8sC@d(4Uw2M#PL)Q-_?>|9 z+Ek{3k-(6$jmC%UQM#IYI&VI@K!A1BWKz?zR8M9kqO2(HdY7H6-E4yPKQXk!UXTmF z_2*P1G#2jM2C{yn|rlMmYey|Rd zIf!}cSNKIIE$$9yZ{?1xsXEMF;^musJe8TGe1E;k9Tzb(weRER<|Ax$7e zZn2M%YiH+jDZ}oE+sO=0(~5^jzmg#hAwqD%SviCvmHh&w#&05?h`q4fH14AAtxa7LDtw0NOx!CA7izESRWtq#N) zY_5%vfzST-OcRtaeo67$QskT`w*a1O)=F$FcFcr5CQ~$(x()C0;j#r?qlgCPg|i*% zSpi%P^2x-G%P61?=`$KblRx|(oik-tBF-0bL59Zo3Tt3!Tf<2$g zxoK~E@j6e>mGIuON9;AI>_4ieu!|n-XEm!k4K~fsrh>*fE~X#Fn69Mfz!{;vOJ3|j z*#t8DD3rEEd=?D~prH7OF~vkBWSUMMl(wzzfl1E1gnSkMzyq|DF}iBU_4tNrWk5tC3G-&>-2xuUHjR3-5?2@#2X9P-@zibh!S-2}dVb>|-m76a(l9_|xy z&PB{20e>4`ZmYuaHebG#y*rt)?CD+W-}k((!f>Pa)mnp9K{P&wN{3w+<8KT!?cd^w zcx^TfzYpg~VH1Uw_v%e^mx50hg|++6uR@3C-9SFFEg#jjBr%aRq>%v1$gXykMjI0C zGZoc}y>JrEi~oqwvV_?GL+;+T)(C-Sc1U^tSua3!@(S<`8w(b#yT2lnbL{4EZR^4T zRK&T(h8kIy4EF~I9JW!EvWDO_m@iL9h#N+||3X zqy5Rx%*uSEc3t}_Qw&L&w>TaW7bmYm6#UT(G}T2gOWN$7e5chG$KMFcHZVO0)>Gx0 z08ugm9tA&|MhJ}rA{mLkPm@-$2KItOg+i3}ve(ypmgY!w*-U=PAZ9=4cuU(z6M4Iy z)8zWm7=a{s`8qTMYy@gYAh;4k_2G8|X_~+U8tV=sqHA2EpW)0!iR_OS-A#z|H|VSr zyrH4U5lIVa$fKe|0=!=B$LgSY+(?kKFzpt_h>?+qzrxF30%vCuQET?;xgqMvGtp$; z>3SV0yk)Sz>P%q-cId6YSU+L@PJbd|V3P6+*;}cb4=drRfr6(LoP3XAubkJh8#-Jy zkHLke+}j!!$CQw1)Gt7H$9W`aIB_jus}op z2H3vjc-kkHoXsXXTZqR<$_vB-L2@^}N5I`s#|I6jnfk!#9sGf-PtBN%{oSu~1r$fv0#HT$^s@C7s2uYUu=VWsr-v#RsneY4 zs+(dh1HZ3x;l&To74R1}T}q03C7K2pb=@JPt*Pf1il6jgUHi-PuXw`!5(f>W5yq4fNc8XU6WX!iCZ)p8N@22VNafgQOCZv`1h~*S157bf%gM$a#q}r)|L-qQ=ID$1e=s+u|730) zEDZl!bKBOGNZK61=$@_ZqtO?!E_}TqcO1^rZkepNW*QfG0K^m&Qf-R{oO?K0J$()Z z6ps(KUPnD;9Ad`Y-u>t)?i{;mApHC~IUJq_pKYHu7qB+4XmL-Hkb)9miqV^tX;C+& z`!~fnzIyA}q&9M#a{JY1GUV_vUv84Zcx!$$8{DgdnlbO*ZgB_9J9_n&oAOcjbiG?UJRZV=x`>G4MI;DYBy8K->Hf$YOLcuR zQbDwmdR>~?G8u5xFCBO!`S?9v%gmq7Me>G8Yy5?sM&V@ug{Q{dI>F;s<6a!P7?4vA zhRm3zP|yz1a5F(ZF(}@NR7RR{S;=~vl9?nY$_(^Q1R0mpI10fBbyreF9H*-%kBDzD zqNiuYFVvbKAJAA!5b-qe`%5SpNNGU$xH~KX%+NiB-q^nXzIf+oPC|FO#3=Eo7D7$@ zGgps#5{Oje!D<#?o=#{!eOT3@or~C{Dm%A5WM+hwH`c8keI>~;l+Uyl(`1m@mXB)^ z&is++Th{OUA}#>j!JZpJpOKhe;j;GhPyXuAWdB@O9#djC9{QF9b3-WJC`#}(0oi5{M2~sPw zo}r-7-_a$k+#N<@GSJuOL5VNDnXV#*siT4&$Vuph^6hM9K9F+k&BChSjOB1GK65;j71di`T$wPpz(iZ}}f5e0@GbfD8X`10AZXVc- z_Xrm8rx`9$WQ33E%GV#&Ad=4^Yk>^1P#}Gbb3HyO(BWcQWz!=&CDEHc)EQ zK-Ih24c^_$d@HNTd|oJ3wSBwp&&zqR?Jfj@hBo8p+lhz;Cbs1 zjwZH&!~;{l=G1?zr>zze_x_}cw@qHS@wE_+mN{D`y4;AN1o|jk{nu?ZWX>^t4yRHG zxe&V`vmCnJYEINHrl5wCCkOf~qmSOtj=`YF5K-0yUbWT{hxZgC5+!L;9Uf#0e)c=Q8k*h!cablRs84%LV&PEcL=6B*aYF6NGFO zGy-m?XemMuC-6Sd9Y3n6o{+s0PJg{}0YW<|P}^58puHzz-|jm1y0Ug}McZY&DEKAM zv5BAz_>*e=ME6ITU`43rRJEioGNcNIymdsb33^ZMw zT)QcA?O_ZQ4HGD%W?rW+=unE$`ON<)QAhZ>x3SZ9X@{;0=^m$dJa7 zYkN)Qrt2fZ%&fOrn3P;aS}BHWnIggw=)5Sr+Q({#j*lA4tGaFdFBQSjAVbR2xdFrt z1Wl02#sh79U96aAeGqlO$(eJAXwu6!J+0`5fC2YQJt32CneJ|N_TDBvZY)ac8}Cc1 zv98*6fh^W%s`J8}cv_?sq!?4c&1#tI+&viJv#%Tai0XEP+1c3g<->EyMaOo57iv$( zE6Pi(upU=2?#-Aw7E<5Y7nMq9ICA~(+qtinC!i(9GzffHj@4SsnemPGN=^-`@CL+k z)KZ&XC-Ab(amcdgB5=xPL&#c3toFvW^%|qtyc|6Rk(m1K&Vo>wOz);G7S zmLGQ!@-?S>yUWLG_v*u(vH|fyIIJ0EhhNFT3PqX~yzstW>%|Psg}@1|EoQ+WoSZ(P z3|I|XXpZWr9NgWxIqP*U)lJ46*8Ni#XZv5dB;w_fpJ(3a-_nZ1#jLpv)*ZcjC%>}Fb}pNz z#{!G7Cb*BYd3(Lv{X4BN?S`@50I*A?gUq%&TMvZ>5pfzY>cN#|jHYYi5O3+f=9u2C z&j@QxLJHCPIETsR^VcK)HfDi@>qx+LejbhQ)yWN0i_F9&b%kIJ430-X2@1m^{VZuk zN$f|OuWMhg3$~gsU-t8kPvmtF5rU2(M?Uj*MMN_Za5yj5L}T88acg1)L+~so*iz$i*!1*P)M_7UYO2)&BKO4%#WbpYTcwRV!G_qF?u4ac#9pSrur+x{~?p-AodlLoemtnc1RZw9B z^t?P4N{*GhyvT}e_k{w3O6Mxfu((~n7E7%kA)I+yn|21xrXMT?K6s4uEtULNnV_8p z#do*Tl=`vw21S5@p#%f%XmNGwj0FV(iD3kS+n!xKTQfEZ+9;vjmkdyX4F`m*p3 zR`iN?BLN2~rh0d0@uF6K})dZyLkz*_5-tT@M(}R}JlnoSm?iqX z!z!p~$pI_~YK7>l%GcO3zuZAbmdsk2&az5M$!LDS72EV>=~d{lLsNAN!*`7oO3ZT7 zVJ3d49teAC4X(0BTg(LqhwIf2Rv`ur=&24{1$}diA8v zp<<|V!S+qP`P2nldZ6FCqA3;kO~Lnp%Nl#6ftWNm5OL30bR^0Ud)$+yR3G zcR$$<=iddESo~%8>&ip3po(@6OsCl7%H+;DmwOEfEZVTHj6R`$!hoBz0m7cY2EmCXu%Gt(;);Ky8b+(;0S&{r+^$Oa_EJsA|hw1ccnc7Lui=1*=B5okGs~iVi-0 zKC}R&B9x7#ij*OH$P@u0bo9~rs9hsaakk^m&cdSncC@`8RfmW~C8Uj5JKC|{8440( z1lvXkF}J0~@VfJq6MJP;SaI|q3A*%5jc`5VR5;76`>mc$rikQBh7G-o?Ig8+loBr= zOcDhcjvbD=-y2mqx`w-)r@9DNXC=sGPNJk^{CeN-52mMwPocrK<`K1+4wLYc3&+0> z4sV^ALpt!plEc|CdD6*Fi7=p%8vooh&WABQA~ zOLlW-RK|!(rI5I2_SL1AbRM)VY@^KalK4{Fe6pR${Mp?ti#Ji(6v#%QA1h!}c`-{l zEASJBhAwBVsibt%Q*LXqcaoZXmcY6(d)>``B-7*LG2 zmWie>zypLWOz05}m|Kni{3+|g<57A334=uinEy0H0m0{=Xofm-{3D6f=MF*m*Iv0| zUPtO=u&ibmj_~ACj6dJEtd;?}z{krAwQJje9jMuy;VXGY(RkPLDwQ@&cx0AEU$0b; zO`pZv4LGZ@WEvR!ITqev03IU9W5Ej_4ta=vzjX}`V)-EG2O5L*11tz}nEhDo!WH*N zzI)}1ho@qU4(>AH$rE57*eiNtD@DAp!*&dWEUHs7$mt$NlPUcH3aqHs?iqB|Y23Cr*uk zc5j(5qYN&@ZYQv4Gw!4A!w)xWlJCj{PISrdrU})>hexJ=r0glv&*$4CYa-r9R}YUoBmvKNP^x$&B9(X@4ughCJjWMm=8zt%Rp?dY7#Oi1$>q& zudvjpXfd8w3oF z0}j=RV22F7p@bD&z-Th1Kkm^l;~4_fRDJMhJ3#E)IK8|CYoOik!={Xw#bNDR`O5j@ z1cty1cj8QPKz!0x$;|#Zo6izN@j8r+i8vyRoU#G!G{@`+?D&;sd_eS|3)_=wzUxKn%aId)7uvdLSjMFo^|o!kz&v-{sU z0Mt_k!Fu!ncD4fw?&gv*<>Ct1lIA(XYlO#?{$TnFDoI&CI zYL8KU;_!&eWV>WrRJ^wNlKT;pK-*83@lN*nA)|KQzvjVmZi%|Qo|57=?ZcWd3`#3e z^-cTHwY~~{GBdiC7=6c!MQ&jWdwbKn+cGlQ27h{=g<9H4ixq>3Zk+M6ki4{l`P%XJ zldyK_l!P^fCzfcpllz7HFTM(9PrgT!srwoCU;Pm7xYW**`5UyCbTRO{`KiJlqy3=- zJ`ADi*_`Gd&99u553GN+UfB&5)^7U2R>kB(pDp;W(c$bgu8!0zp;$NgwkYd;sHLWt zXbov~(OO;0()$_nfnw=LSwh*_B>QgB$i&in(L4YQG2Gxm{U!}BaZu2o>P#zyfMD`)XgneB()i&nx=07HgcbR z!8%cA{?V$DxYKu%5ysKPs|_}`k-41-wmGNZ%>+}L`T|%Xe*BqR=XshgQdQE*ZF&%{ z(vUu#FpiizXX-U|Ns}94sz$;@83IU3j$}NqEu^r=N!ed!+7M6Anp9 zQpS2jUKA50-<{K48-T*qeAIhH1i~}1Et^~MT*i#78OYY zu^yT5VDOnr^5A@b&PcGRr!0fJ7_${pCGn3Dv3Mx)mM9%lGo(|=*)mlJaVcBVv{5ea z6gRqcjqs5!QA%PTl@4}6cBMArO3iOq-DU-g4+(bKvRnyKQKHJ_fJx7-Qr#?0LqEo* zKkg;{iH^)sd6R_P>}j$_>Slgq(%^qZGqGf?Fa_eb3g^>km_TpLBmh8>y0QS4U3N4e zlTp}MV%Su?8VY9GDO%gtD_+}AHyn*F9N%?%ZA-8eo0TrJFg0Bi%Ly*6dk^bLq%YYK zIP(F2H0g+_PnQ6{qmim0jgn0;qJc35=1L-*T`sEmmmq{>n}HhV0yWO|nPTQt$5fUn zClenmb<-VyJBL$uEpRAo97%Wv5daYjEFsXgQ1|qK8<9Q9>{6ZEEZkskA34-Gqcy{B%qc)wwQou?N{u^kyn@;q+1~U47 zE~2?SeA7%`pQ{(N(@LFJe6N7Ht`0#0%~gwZ7kcACSiAfEg0ks@0B!#W^tSqsKzp_S z2sA&d5h4J=**yY7vW5Nuas?(*M?*3DQuowZF4=&Wc;Uk+Gu<*2LuUwL1~3R`G?Uqj z9Q0pg75Fc*2LBgXOaF(gV}<`8WA7MTX}3j<#PWGCs1h~q7CDh>4o$zEYBEjdrH+3IjYKT2AO^$sPm8!MH#tD&@=XJUU{5d_ix zIO0heLaYn^*-E>Pi zUdwOaY+kYjaaPW?;j<3#3wudg8v*!EC$>J-pS|zE9y3a&Yh&N$yxX~KOfo0uS`yij zcl#(sn|0MiQ*yQuiLg2n^Oi;BlGNYpiq?-=6sXfXw4rJQqsY&6w(ao74j*c=yBe*W zkcRY^nddc6#@Y}uMgpnm+$op85qtCvpl4|CEJ1RYbL)V`Dch%Qn8Q-#(Hj&hsB^RU zNf>%zbYJIG*x*RDA3>gA8N~(dBNLWhMj=Jjwu}Y-Vi*UEJE70m{C%j^fjxAsQyP^V zAeS?8zB*M!P1F=uMforftr)x7Eb%?T2&P~_S|tDKs9;hgf(ucj?(}BsTPV&WI;aGS z?gN@M^2{z-wjpWx#Yn6CIFgTTpsPf|z#^_!riq3kI6SFYrVWRvQ>F@6rPP@J)QZ@k@|yV2D@#%t{#t{Q?-IU=e^piWC7Dq=Wu{J%|Jtj8yv;M(SFeh%8%@ zd;2HlTkI?DhN}(=V`AbU#sYw2hYSC>my&?%;o|QqX=;@j(vyd^g90b<`81?l z%w+;o$&#O2z<32xV9_1ERx=LQ9LXe)mSpXBc7atuf0$^J%C9r!yVd1$ zc#T*HlOUe9)d+rs7P17xxSzx+&!WhW8BvkPRa^2m`t-D1dwsnYhJQ2Yx$}L?InYf~m34`6;V0Qw5=sxA zR|T3wwU-S;DEQzf^=+R0FJmhQ!+#lDnK_vM*Rl0au6}Yh2TI@Wsq$^a&5G6oV!w@D z<#mJPMT?d7b5eO{xa38Y!8BYen+%Y7a>vGv-%nCFkDQo9&8qSaOygjM)}?8ACYI{H z&PR}&<>B2Lx*lIiV?;)SVY|?A-{)bGVo7dVOOHm38ATc6_TM+W zo$unK3wDJtJh6&0oEPkgpcRlZFr0=2sASr{OVj3{sM}`gFs1Rq{7PLm$9x3q^)H3z zb`svky|R}y3pa|~%-VN$cFU7&pZnsverG#X_`YvQvA3()WDsQr){0 z0@e8<6j=lE3anJ^z3^KRi#eVc8@e`SM|QTwO{ueS5XxCq#9=CQVE6p{)*Y_Iy)8&SM$xhCh(8xPYH-*X zF{3->jBAFOJz|@W@F1|%^_NFtHnAdx3Ui57gvp|j2H`l0(2Tr(4|%;Xs`|X6Tmjvu zC9qKF(1XM6uC994PgJl_so(7MM<5t~PzUdKDFtsIAJdz~d{V@mC=sL)WGCwOzvP2%BP#!77!$i8<=cR5OL&V@ngz)k(UEiicTqc5Hr&7k6x4LZnE5maWlHAgJRG^1O_Sj-u`sv(U($?+?XGdoh9 zV{b|Y&HBW;BqUPfi(_;{d~-Isi>~o`^JKoUW54G;xShXUc}dh&k-}P4=JRa^9s>e* zp1~zO6i}MRIY4P>aRH^l=m3;P;@{F}Vu?7kWZp!Mo_hDn;~k;|JQ4c|G1BD@v@p^E z3u3wO5{(_-VaG){aSF!fddFQw=l*X7A3Rg%v7}2q;zyebr;>lks7h9E7fW2 zVc~E)VMIn+@5*8n`CFCU;NejjDeJs-M=bf(b`?9?X-<9m>e2(_DL?QvFCIO$T_ZB4Nen#R{iqf zRlK1;$0sbomLlW{FOgY!`6S5&cE|Sb#lJA zuUJmBc>aSA&d*a)5n9r45U8>a^j}}BJY9v>>YeR|Nj8@UFPqeF*W>yRW+cfUyX+UA zv6Ir8Dz}CpxcQupprMuJ37v>{+>d4_rylK#d(H+36zAtvgIFQcH;gSoqrXs@hd>>D zG>i#&KgC}OS|s(%A5ZDN-n`9!Rcqo33Ig`3c)d+alj8Iz6I#v^(=m-3md)F6g#QED z=#qeflj9i@BV0{5jU}>wsu5v^CBQX||Xt0k^H%x9o+|n69Tmf@H+|0wO!3#P8*8_fELb+G`}(Y^hrRn>PPBf~5o zW|I)3;q0_wpiK)!;pS~LZA`~x&l{Bm%I;#w{Tfj0YKrJwYm85=B@z<=-=-l2)?D9=`aC)3JofKvLHbl@j%xdS>KoV`$=R z!BaJTbC3ux27)r+vdz+OJsRQ{HTIZEsFrC7!j-1Kp2(6o*M~_4BF$~u1v>0j4^ZGs zu!_dVWO<#R%))_hTE><-jiwnFyBfSUC0S)KGClcz>iEGOv7~D4&X3$e1clebAZOj! zmLMyQLgG05;y&sUMsX#?V0A3qpsbD>!;QEKi4wby484uvj;e-M;F5+E*$A23$2XNq zWwS__150I->PD8}np`eu40vM$Sbr|0D=HgkL2615R$kyB@ugc1FnPs60S6a-4&)J` zc7ggv)Q4QRd%3yM=v3eAewyNv%JA9OnW{!z1FXt$ZV(-!E^@q8|^1Rzf=bFOQK~!zigNQ^vflV|L{SY z7K21?T=9-LO5^$6#EF~|jQoSr%_FEu+1~7LOgcBb2gHd(688*OO#92}GnRHg;Ia$w z17aS&!)4U*ZgFTqU{{Z~f8Z7ZfSYh30B%DG0Ju5)+vL+kLW&ah4jXk@{b#>Ym)&bk zUFjF!Dm+@A#ix+b_JP0?J4rXn*_k6oCUe7tL7h1wiOlgv>J0va>@>lQUkq^ZFnFF; zul$m@L{;k54oSkipC{mnv3rsdP3Fj$ibr*e7Nj|Y{}Ca@fW*F+7p&y6^zUSmxy-`O z)+hku;K%@qC143qtipd3i{)R%3PFi9QXu8wB3Syq_ebeR>PL|8;`%f*%L(1H#A0uX zNRDw{wS=>r*<)Qv8fB7$LIXEDK>u`n<)@hOnkSonXouR@wbAxupD?Yetyq}U8|Seou|&uOKV_6mnmjJq}dat`D8 zrC~dNXFoMI(FAT@B|x61r~$$2tN_6^j{(7r{!P{tJ-D$vv(nH9tO^ST!lFZd!i2LT1&&q`}Wh2nrqEuS@Oj!m(HfH6fuh=L@iUobM)IqoU~8CpPQ1re9o0%Awok#zG64M`L_ z`JUW;&~7^EL_M?E)-IwrCEc4Z7IY`QX{dtLF}WzH#Sz#ZumD3K{8^I?_hV25SJntA z*+j_vOJSU2!M5Dp^qpoyg_>TX!Lm^|g+>#*q_Sl|cydOEo@&r9d-+pgAlCZ3v$j>! z{0NEX%gDM2v>Gq0;-;73qb@v)yh_Clet(sE6(ae4a@ypzLiRMV)?_!$i4kH`K|GaGwaNr{fR~*hazxMa& zQAr1BstUCRTu+@OVc_tSFv=mMewcxg+kjr*>pSk_by&AdDa|F@)Z8*}nQGF71lh7e zvI8pK=t63$k*QniM$Wi=3Rm%cKtA)jNFjK>^hImoP57;^E{p3Fvrk zhf*N7t~J8UN!oHFffp;D{UM^5Xi|(ginekLP+6N2d#pu=N)H*b`qr0nvg;Ujs9glS zANP`u(n%p;3AFB_=pS^K~&&tCmBjaHG@EEa#{ww4=U7# z1U2Y0s{Mpj;hNZ4j3)|t`cK6m`V1ISWNAZvQP_Opq}7Enf;A&n*hhQF5D7&H<6%^^ zF@qmgKl>8422ksyas0y9FUY_~fhrsZ;dTber{?y->@o_uZPLIhiCTvXz+1>kV5*Fe zq+#7Rr*NjsaHFGZ9Wx*|G~jFdnJRgCQU&C?j$#5Y!|;NBQl25%FoKpgV=xD3t{6d# zYU48#8cZ2OR26wn1u)d(Bjgy9<3_MrhPpbtG-c|2f3C90%FD~UV`iT(AHPSxex1sf z`P-G6cQ(dvHnM{QYwpPL%kj(3?#}r%X6o7T>lCwBpJ*A4SpN`-^|eO<{xg$cP?%*`RC{HtnW4EQBN}&%ZjjIhI)6I+ILk&Rh30~ z(Luo~PryruwuM@05GGgYJXbnZhu7X9-A$?Yr^99-B$>8$93jm%^-$ZaOPg4ZJA*}- z66vZP-$WX6hrTcxHmSq7_Gy_Eou312%nSgP&hD9(D8D26CDrIa;wFSB`#pRjn9JJE4_= z)ls_~{OoT$wcr)?qM+R0qH@0{b?KsR0eP?F_%(;FF-JkE>YUpwYO=u)%BsCsUDK zH1o-n6mwsvOsoy6G!5I`KtXva1+puNTD`tn=O?zOH?frW4 z&H`7qLOP2GTAZBC-`z2YcQ3!<6IJ^C@K~f;td!rMx0oIMU!K6 z4`m=ELZ0(I@nb0{qL4W2FB199hi_%X6^WX6v_~2P_4Gu}WWHW|I@=pNRThA1Cq<8Nk2*r9r*SWX_n#4<{p1YHt>S`$V0ZX2Xmn~(%^M`% zTJ9XK;^=o`is*W+2)9YmxcdQ;krIC7O}l+Ww0$)1lKd^mzJ7QlMkb91hVv5m*9Zyj z1bTJQ0K#+V;01~+y@l|V+*JscVCmPd{Wy}^K?9dVk+=XM>8(vdtVC>a9TMv3zNs15 z9)TMwJ4cCBntP-h#JVT9v@YR&?s^nik?z3eqyr#JDu)Bynm6^~fQ4YJ-=Fo21*SX@ zfOM3u`DmY}q}W1OnTxl$B5c>69lZ$DSf71UcEYQJei|vzN%ai`J1O#E;G{%DgjmSb zxPhg|Rflq2+z%LQun0S1@rN`gwIfc%0`$-+n%M9`kk(^mw2h?u^kvi-iksqCkoLnM zm=E=(|4pL{8pYy+f+@&U2oBz8+%P+KT@3HFe{?12WO2N{nQ+L*KG+F)^!N*cQ)yS> zL%;0pM)yP51hMhS*!f;HuQxe68}ZnTs3SkyJ_Ub#+Px08-4hYxBbxb9khX~S%veJl z*8^`bt0)&gC3>d!4^7y!q4l&JUs$X&1>no`>`Z3pq^3!y3sxED<7%@v^|$ZDZTbU| zsj(~n-wMdg$ogMaB3b_T4K&NTw*PFP`MTEckAIK}MX@J6d*VvAPL|ja`qY%q#WkrV34AQv#g?2Lw_9|9Ha*F^%arT_Btz57Fx2f;#Y znX<6=#+E=82?meSR;mNO+PK$x8#{wh>!dqHN0D=X`{$EPdLW!De}!$I;`1)4Tmkd= zuT#*s3fre_OW%X4nt;=g5_-?a)){!0=-@|E*fB^xYC_g@dT7tb;ahri`YK=-)rzS1%v)lf5_&!l;@)C#ewXewuc&955d%kQNV!;sRvACM~C{f5ObQ{R(cDMB` zfz#>tGSrOWA3V}L%q%i(F>Z~8gz`NceaTFP8W`e5&iIrmge)kU(6`C>P+B@$T*_`8 z{qltV0_?Svk%{S-O{$2tBD-?5$lutpqI&O5N}{`>u1?QiMcA(M;q{LH_c7b4H2TIr z@4U(*cloA4)6U61;Pl*GZ3G%`<0ki~!Z4MN^6^gv*9Y#O3bDvOXayU8BjN>)9p^OK zIK-D?(s+=5AFzOjhJaJAfL1gHN1_;j`HS=7eagX+lYkLG%DmyJU;uyIHHJAP9b)Omc;r|gq0u% z#e+)w#(O|Ze?i28hX1Dq!iH3dq>@`$;ym!uGQf1aSw_Bxkbj4e4RceNP@Z#hCR0q| zaEy#RBs7EBLe-fia;v>@t#+&xK-+Ky#mYO}0Zj|jJZ8c$HU2JFXLn6zuM}Hl+i$75 zYJ}Ztt;@Qk&Ob;4lL-pX%F>YwDWE*=@2Rhn+G@K+&R>1%f8%q`#;LhlobH}1cQHJH zeOlgv&ONsb?3i~5+r*Q>j5DlQN^>qJrTx{C*CK;2TzxGd-{?KCL=8P@W!9=Xw9jK* zngJ^II$~xPojL6U9!T zSqGAQ;%){jW!%_@|Lhf4DWw3P@brfUN{t!{lW}k_DhN8fGo)F7qcCOsR-BRmrod41 zG@AQ`KGD#OUNtgW#nx#148+IM{)Eg6FNLn^w;!&29q+BdfIyzh-T1=UT5?2l&ys3L@HUG`VG&GY9$NcWn}#lSziRE-z&n`$gQL_}G}_Y3N)pia zqnI&{SFePwqFV{dbe$kvPoR_=#esVTzX)uE3CPWX=%0*9fp8Z$S`y;Di!%ZvP<#_` zCX*z5GXgva*sB=-f-Wae;ta~N!p$}^(49(#zW}aHgeonSvfqZx&Ai*&_J}mThsw0F z)W^$<E*Ehw;8>2?$58T;oNF-&vPzwNO;4A=~x6c29^HxzTW3RJi?dtV)+!xzFTXkKBy`#dBbtFF3AaR5=kh8MZ zVQK!R$$>@eR&mb)@O=joM+J=q_i+`Owf-t_1g&!_Th+>PLBw^C6nUv`n<@LcK052} zn5goJTD&28MxS@@r+C+W<~{1iBS}LiLHR=}0QerG@ek+f|KVIcfOFwE<`G%*Nsz>8 z3Q-O-&%3>gqE$ZgXPwl+=UbH@jd{B1vT9x97aJg}*$_A0W6MqQFCm)HgGQgjRRG$L zQ~_uotO=lfq$Ys&!g2r7o{=UJT7Kz^+dr?b3h;;{RWm&6QnWD(=jbnJ+Cc>6^__xM z(Qc%nWys!PpVBUg-at~X;9=iB+}FN?1-YGOcg&W z31}K{68|KS@J|8>fCM06|C2yC2nvVbP$YP<34y=_-yuXM0`58<*YN?ki3f-<4>?ux z*T9@uFY4eNRR3sTVUKuM2oa!H2loH?W!7|5$UsU=T#kcT?rUHHGl;(x+724w*QBXO zh>Ut0&@0fSb~D7mqI2zN@cR^jUU6(Qz)`{BE#e1_2PGJzXt4`Pp-VhENh9AM$O#2G z;)^>Eb3k^EW`OK~WC7U`#{#m``uI*+mEh#iH2x!WJwE;38ibtZ**qt!Ad11&b!`pBQ!;FXaE0Jk*7e_Vje%WT!ET5@U06>oZ5XQ6GxpOXW#JVp zj||8zlbvoVoR(J(TNVh^;@(wxsyvN9Ble*4X0tc`syF8uIzlhGv|C-=?2)S)?id*1 z&Lp^}gpCysn8PW~6xXJS@=asgE_ObUc8Qnf8v1SrU`SO5T?mWW)eH=*-Q zM}g3Un94Uk)gPdyB@&5W^;-hH@f{-p&!<7H)E++d*a%5*qj`x899SAXDa!&Wl{m=w zNdF$Fh)B>-!fuNi{VZGI6ME9r)A)v}yR5F!`cA2=IJQ_uBKRp?>DI(mtbN=ot|PPB zBrs30(;kFtEl(Iv<1VUZT3>2q+nkA?MULACtpz0yl~T7&sJaClrh!y?)S-G~;bmYR z3J;`d{ZseFS36rz6%CciN8RXBxjv$wX4z_^s)F@TRgfRFtx;i&{Jn8;`t4%!H!Zi$X#vDzdg03|BH2Z@nGRBV`pA9=t9f>osWijlq<4OHp zl5mA;&4RTTj^w!so3~s&!d&c4sGXHL3Aoe*A@BM>4OQg4aV9k3C1@QbX{4F-b%oa! ztf&~ET24Fhhi;WS*Arz-`}?tu4((b~4Ht%gdNg-7;2dLZFHcN#m{thSw>r zPd4Ic6HNxS+iWf^gD2#E+@o7EuNu!DyaAVHDqpMuGpk`~s301Y6eWc9Pb zkkHN^W@T}BQ(G^{33zR#*(@lr30-U1z~`|SGS1S@aGhPg?#L#gVFzyR{8YS}2;H#r z=iZMg4bX+=MGEIuJP{LM2?JIa;wB*!<`pOL8{jXItuq5phyMMOh3k;D&N+6Sc1cqj za-8b*Y8l@i8df-H=Y?GVYgQE_ab$}wgl31G0Q6jSXlva7T*>aQx(Vt(ZC5u<`%~K3t%fDs zUT;lJ{DO0Y96wS9=)84}(!Fmca#M5Tw424@1v<_n+%ZH zYZeTMnx5dpunlg}3rv^tn32Q{6L(d9RD2lR7)Gel)m?q;iZNymG-}}3+FP*?T|Mu; zm{sGvkHx+cL8h;nu!njt!~2oT^0^OuGYx$}Qk*XO>YY6`FJyQ?PUbVUF|BH5fI`Ep z&ZfY!Pf%Bfe>azQv@Si#j&%U@EWw5uU~PG|7)vydM%ZQhAvS{H=ruoDV-jCLyO_~A z85FSP3i+$4iJ#6XV@WAY^yZB_i0>yR8)8!p$hf@3X^mtXCO$KH!5Oe7^24P<(*T4F@obOlkCk_CrGJQEx~0_5c&KLOmFF z#vEb3_330m$IsI1R=Qv0m;a!BmgrSlbPdnvS30!rYwAUF{`*fOK9jd({MEMh&Q6B= zM*GOpJN-v$^jZHP;A;<)ay+DN>5yc4z=ZBRI>esV z&3{ezn)t!4Ywmsj>L%TMR&OMfVxRyDc3)S|TM{`$$>YZdrsK7)+K%>jv)Z(D(WO;= z%;DO%XLAIT|7LarU=8^35AlSrZfo{13M^i8uNr(yd_~v^1GUrl!C(PS2ANMIlHcYY z2XwVnclqN%>!i9lX1e*!ESLhOmyx&(N^$1H#N+zru25}IkGdY#km&XHdZM*FhND6R z$VBt=$(6o2Q9c8YuM%3UP&Pu;_RqX5?FUFz z1P$5`;P|%V;OBm$%yA?N_pG*D{exLglt-)u?+%=Jcl5 z&_9NlViPP2TVo2-L_=cWAfhPm{B@}ccxF&~ZB}uVFY~NG6R=@>m0(}C>0r+DmBV=n zF|O5?SpSd(5le3ACBij`-0B7HZ3lFAeN-&!;ClP=uBiC&U-&Wn1e&hl!sM>OhL9w( zTuzVXdD4cc`qeA8i_EY(c0N_>;wuMV;V&)>Aq0ux-L065?b7XQhkQkNDjFD5y!}&7 zX+svQk!V)$GsZFCek%4l*I=Fp!kr z-WmZ6Ewss~P6>o23Bl(`p+vMLH3^Lxe&*=cEoE63ZJsu80pGZxWRjB0+835M_#iSN zlZ-&1l9mL=UP<6gGiPhf1CBloEf~C%7{}M@kP3Aj zz{jO$NR5@|wuA5U_+u-r*0?UB*tKjW)(|~41SuTWu`iB^lfi?dk_l6$d6=~KJkOK$ zmwQKqErDJIxHpvw799hy*ue=3ZE5s#8ks#;4+egxp%4q1bAu~GcER_wvJ3k416B^6K%yIfmEK#8LV9+Qg zzf!3;X^LKi(*CrS_iH(tgQ|n14BkxWYHF0?x^Xi&81>>nE0|T6n=dw}Q7UuB3;8pA z&S@tGoh8xcRjy_-5vUfCW}<|5RNG$9mT9$A${w-VI!syp6osLb z39KWI5V^8ZXVH{uaJT7Q^Ws#1_Gc=>b9db+$K>vJI6o+37q8YCuSUltHB zw2PM07MLx5Ve(MyzL3_#=_u-`FD_kq1If>)jo2gBs<>uzNRl3V-q)>ycaTK$JdqnB z>9ak92;w}_lU1q-r|(YVOU2dk$hqVY@zX$4K2|~8{M&;USxnBh5;p?m&hdyB+yv7^ z0x_HQ>#4dd9#nUD!2yd$;BH&x!eq)19BeR5m?eJ@++B9iXV2hEl~GBt9v}CQoaJC0 zPL5)GWK!VF()`T~zYyMqg{yv*U$^EE#)D~w(hkL9kn&Di8oC|*k zHbrVPPO&(1#vS}otib6slY&s1NEjcv5BHAbQi-_`8g5o;5z&xHVO##dAhOKgfBRo4 zfFaeGWIyjD-Hkq$VDA`@PU4fYbK5lH6Fa?_+Rqo;-am$15gKLYT+z&AoIZ@kIY%@TuguFYb|r0poFOXG9dN`&e@V77feCF=S+{k zPAL||$DcCFUW^013soU^vR2UDD{inC>z54*6hyg$@gKawakyrEk$tM9_{*-W0ya(l z#HAi%Ra)kV$C53ljzxtO`rU2zGZgzRENmeq;;g|LN5?yWvEx_TdOB6z2s`MsW;TxW zn80C;#KBTo2Ab?-VUS1?1=fUT0il>Y`Jn(X*N3S&+l=Gj4UcMO?q{9s+1sv7IgiVp zLH+AFM4s{IRLtkpg4~RSli7Ai{UXo8h3DJI(2O>Yn}($J+{ zm$Gi}hXWFjbP!7>6Q7XGeY5XmPJHIA4c*UHF&{Q5o^ zKMFj+Ja+RZqGpqa7K?kuU9m$bds$$USR4aD&5pqR4`n>wxgHpM+#YgIs23+~K85=H z21fEdjl%AAh!Hw=!(?6>d^!vED__tR?vHXQSP4;S4+zW|MM!xsDN4_-#(03(;g({c zLZUq*2NTry(fa^PO}BOy#*uppUyz5K+X49J9pL&0ulMX|;DF7BlYhj%U(+$f+1TIl z`~^=R!8hCZ<$gI&j%C5XL%g`}R+VndKSNqqP}qE!vMlQm?-rN7xd{%w9nchfsCka6Lcg9$NigR)-*8-*5)&hpmy(U& zG<~lEyMNHS0^*&r3z&0T3YX@-T}!gJd0-q25W=XvSHrP6y5jB8J-B7_kI~%mRCVw5 zAozXKNBN2aBLz|J$5(ii)lccjrx8(g5Xv_v!P~p`>GT_SyxEPILt6k_1h!nuEwY&x zT1!{$LZ8GA6@!&)p&3XH_2|Ivx?5qbW%}m$6gnH^F*#t1sBygdiA4+Qr%SBF_M})% zT{0FG&NtuO3|BeuOSmE!BH?=2id>=?hMDq3Q>)NS?D931Hq10h`9%9EScl{^f#7Un zz__q&BZHEhWZ6=Qb(v7HYc@MdM=4%pLYcrQ8^M&py4x*F$-J`4e{TevRX@hzg4XSI z;h|Jh+Ic{s1Xl1L#u%3+s3Q>2xt}fc;iA#RPV%Hpbhqxo_3;_d`FY)s+tVZ9Yljfc zVV(*iz?h-{oiw^sOUs#=zhheFT^vww>Y8F(i z;@VHf?{iNQyp($(BLnlM5te!U0Y&P?8~V6=4{zyOEr&7%vr|8Qzm3LHwfOG-kqm(ys(>QNCjT#dou^{}PS0s*L+u=D!Y;L6aLP!116^{UM(?JHYsa{v->mw%+ z0;Qt#A2Qi@QY0Y9YrvIhCG)Z=V*I+Fbsb)hdF2tsky7Z9YMTdUhRjVvLn^S?z`-TdazWy(~J?>ako zmToa3C&44LH?9vEDxbqAX#KQ(+q_yk-rPh5I$4OtO+}I<@ok^f^ZliZKvwufEC*EZ zw>xs*BWThne;MeA=GS~Vr~V2(HIJP$aEbR z`*?#edvUv(|3GH?F@xq2wXPj5?a+1|zOo0rTt|OdO5=@LDk^8JW1(fOD>6eX)ffVM z{Bz;6=NzFR_l{`ma>rVEh276=aY@v2nb&zaNdT;GP+BTU2kA!MvE}?;so3%(xx9J= z;@(o1mClxpO|$1MWsa53Z!X(R_ks(G00I5?!mF-nH>)kq%zDf8PJB-OpRgGfGiDek zz2}CgcdeVr*nC9bTlw7c*(SMpN_%Cuo-9gsw_LOOqbl8nL-TFRN)`2G--Jr*wN^ap z?+DS~dB zm&*G@c%O8?|8gP*>HkHWti0SbG?sqk98R9an5{)n>c%+6zsLRgt%p%6sxVR&q;$d6 z*jjm+0AWEGFX&#hDQU%~T?LJJ{^wJtM9?MYqeTai1g-51-&S2oOel#I6wI}y)`wK( z<9av$qoC^3zL$oZ-)R|IflH;0o%XDPwWIAB@dSYhtcrRjzCS&uS$H{pvVjk-JKsi@ zHLKjn94}0-He9OJ!_8I(89}cT=C5)1M7yYI*$nBzT4~5wvPwboW`7uG+a!d~czcBZ zb$yWAG}1*!%V%AGX!A`$;hgzAkiWu`HHn9M12zjPrqTk}bBh+Ds1+{MObz#aNK7i&Jm==ID7;46~)jJ2v zblo#MDm0W54185X8X&E+ALkx-h7Nz=|B%JRHP2;D;l=J)H10smxJi97bUxqY?7e>} zdIV7!SR2Xe7+AtrTn)0tIf^M8OPooetgs78a%JASlJk*3Aq#mX(t1^X%_>Gd?D!i=cE?`|&Zp&WRq*gj`e`KO^SkwpMhSAv}Z zh@=0-0hY1A4Dt3L(Bd4_DcTp$bGEceaI;P4YxMA`cn0w9uNl~w*ZO|qY*A4pIJ?jT z(Gq#ak(1Iv6U-PS9G(+7`?ot0;1U&JP|J^-wY;7?)qc!@*WIvUfG)zsH~D~?~R;d0s`VECHi%ny+2pT1)zHX~{0#0tWkop*-V zE!Z9VBWU08Xq+giH9RLlwi>0Md_oVXCAksj7k{+cn{)SUpRad<9=9}UG z*~6&Zh|n-tyhyD@Pu(5tXq@2uHUP|{0c%!8?kM?!Y3EiAaJF2nE`tGwf{-SvT`jHo#$?D#{Y7Kve* zL@owiCE7p@XxSE9-eh!=)zN4B#6ixZ;p@Ge+{$(a=K_wX@$y_z=Bm&1@O!LWuLxL} zTC+uUS8wL4JHYF&o4dp$u0lv)n0XSVGAU7T@v8F5kR^yL3XssKhPc7tEki+UEjDVO zMoW;VT)RJ?Zhihbm}nAV$x&1tBx(svI()j>;MPc6LrQ%{Aao$^BJ|`v^lU>JCCp{- zuTc^Wb#(gkY8UR+=SV52la~+Jv*;^jO=EJWdVZMg`<-n$=Iin%(}F5pr7>d&Qkq<- zan`wmu-A()nb`18MJh%X&~mMZqKu;wKRXkb3>zS23hH$XhH)k$xtWQ^bt;g0>-_Rt zkiJ*|Zp;we0Xv%=xiOhA(Af7xKGb}w*v zj!JAYMc>Ed=hflyV)bPrcZ52jKbs4EhbhpI+0AZb!9PXGn1m#@h|bVx6yBnN)anT6MR z^RP{3%0vWIH@At3%;$v2j6W8D8Sy=F7ypWfZ(j0+?q{A3ZXUAWnUje|ROY{M^S5Dl zepIaE-X2%v{q&PkVq^x7 zR17+JngF>~w`UY24G?pl?2ci)@Ikwl%#3?W0S1Xe4~u`IR!k5TXKt6Ueieql%1?yj z{JZpWaJ&GdM;?1l^tf$H@~i{J&%Zp0=82w4{KKeONvCb_9gR)tLpv0?HC*pyn0p@l zeuw9bM4!6`43w7xd+E4X%f`n1=2Iv@a)mF_{s$?cnR+aA4-Q{tNaA{8xKG4Go@XuN z?0#+GAnU;^L!1vNA0Xat-2MRj0g3@481r9T82f)YcE`lY@IN2BTh_9T2dsX6_4Epz zrB7H?q3()UE~2$rCk=u_dC6j&g&!$cn0i%ncXK3lW&f$YNnlRVSDP`*Wm)&DC$*A5 zFb})u5#~gw3F3Hpy()`tB+!eZHE>KB{sBvu7C7-sqd{Ph71h$A9%?7FR(7Y0R<=1~ zIuvQYw(`g7zO6n6FB=d0Dsx^~snu#@d-r+Wr|ZtzldPVg^Hd{?&z&O!{DYApr!k<< zb3#{pcGpZ@wMMI()6>oHr@xSjiiOQ%Q3+llM(qb zxz_`;llOT~yQ5nu13L0chN`Di2w&V$hDL@2SchBMon#q>NECOsNJ9IQ)S{|j$0ZTeHh@Z?EerJw%79BM@I}4R`_HU+}%dZM8 z@X*V);=Guqe#=dt3K*n46XQ{wY$~Swd%B%Nn5MgGuLfh@XGt3!NGrg-rI)k0=iSqO z9^*#MooE>KOFu<^gJJ&7YkVn~{kqtlR>cjEc5^ijC@ z-}zMcNsSN?&x}*lCMOy0|4Sd4Q;35Hm=>kK2T$|AfgBFxZ_atU=m?UwbXdOwLrP$;2fgepI z4?aBeDD8(Sn>$ zbYwW0Jmcvak5_zdalq%0{D%6gLv7^K3I!pDY1G#Whw^-LcPq*!UTC8Pu*6jaU!x>* zsBMK2Bj?Qw!pzcFHv1x8-m$XsW*}PjoPMAjG%V08Bh0Ipzgk^*b+tTTarR!N{( zYp~&+z-*XkYI=-1Q@C*3pboUJd+`qWt6%9scI1{2 z$IMN>Pf>*ZNfpg;l=mV{%p=I|>JXIzemL1!z%~HS#?Qxj?0aOM)3#9IB{v?TdQlKDoX|yPcMzdm~$7%N=n~oegKZ2V{Ur5Q6hFSyV z!N4~rU{~Mch8*0j_5DKZY0lGq{Zd&w{qFcozgs1pUJgFC3XUG2&mXWJw>HnUhP`~C z>H=*hflF{60beSCX@Ow!BX3j7rtis6X=3F8@W}$*iAd@Pql6gf%^Dd&y{1x*ZhoY< zC1F*jKS!8)(X!!80S|DLJxL!$BEvY{7v-5LOAEcz1l;k4Gf!zpWzG(s){!?m6Q1y% zfp9XKLb$;>k3_0kEq9X?v+ESukR;#UE*@{qK zg`j||aXvxKp}(mu-W%0GVc@Xm9Uw}qnvet?ofblj;He^%NY5)3w=`)xfQ z5?)uY+U9WYeU3BY4{zvzkT!P8!NhyZ->Kj+^y(Ofm-PL;6futjs=*3ZxT*kjqWhHn z$}S7~$($Dl%lHN>5RcO{3d)tHU<#}0mav-|9>f$E1Lgt0MK*=+A0mG3%U;FH=YlLW z1}+J)+t}aD4Ie3GeQJL)HwW}$}QJBd&5wCp?+yPj`=By`YmdSD;?ip zF2gQ9U^Ww>Bf}{_e(UZz@_bP*fZk*sP5(PyecGT8(%wI|nd*`S$t(p*EvDIrZAn$6 zIrp!#F574UV|U4Ty>$sl?z&;}9~h?O+BB<@g4IbdO9GqS?C0L!xk ztT-deDe8{qYH;SVR?XDi%Vj)=ip<`P1V&t+P{KpR}4GH3gxkBfk_` z4T+mM`P+6zHEux@EeNHL#(gipamIUWu9T0Mlk)1`azC7CidPrMfO8tXVtX!QVfior zTJMBQ#)$J04tx zpZWD__HZ!7Uoa_`s{&% z`2=A?{@kFm>F&B2{sn=&VR?xulethpbbUtZ5WSATTfH$l;}9P!$aCyXjDVu^4^$}Oi%awF_29kqqJre0l``KVhTd`H z!e>vR%l<2n(JOkQ5JFYFX;a^D*azRcVvzL{w~fkt>g0Q}&__^skKGX3%m=|;{RfAa zP{+|71pL*|4H7HG3{MGg`DJz2><1v7bmy--?m?$$Rj@BcrlzX1IP%%WPkeZ`wnhMY1FvZwj2vfX*y9n-r;wvacru) z_0mTR)k%xhvIXDqwE-HC5#7^m|-WJwkas59kJ80dI1<;T^u+=&}HmGA89 zvAVbnw7@5&@OS_dad_hfSc)5Ol&;6cPCFW9y|G zy&=loE^D;X-sllYf(GF$@zl%;WZ=n`pX0n3nh@vbVVvC*dav-Z_eAeaD7mKKCfm@4 zdFF_Sg;rj6FG-(W9OxLXq~3NwEX6Fl`XH#Ra%ed%pKC6lH>TF+48s!+vLyWDfMW+) z$xBMfG!!J)HCN%C?Cg2&4S#aBXege@XWVUFM2 zb4884d2IEd=;+Ey$7c-d;)nuf6B;gcNj^j+Tyi6Y=l!uL+{R-rJw8RP`cTu~gqC3l zOXtTK!hLw*Nn0S}nkTckPV5gPpLg$KtFsjD4`!WS2CW76;i*DW7pQO=!YE_ukc7mz zw_9&ft7XYmV1fYMqo!GQW-^rL%{?)YPF(Lz=z2p+hc7yMnHU&;z$f9F$Dg=%SatlI zwT#80j>Cvy^Hb^l6XwwUt62+V(NBcFci@P=X8((?SSE$SP`A%XUeAQmZO z@0LW`Z~p$?1tnw-#e`y?Os%6%qA0cbYeB`$Uj)+JeKD?{8S%lV;b_PyTwO^{Yg)h299C%LP{7ay$ zt;2Ol>#WR7r(NhxH3Lar#2JQwIbzgc+aU8#8+8a1Aesy-*HbDkH+r^=2O_{_m!1*= zD}iQ)sJHNudbIJPv((15zBB@)o2d5Q|a7^TJ)x=KXfWl5Pi9NSUPpg!G*sBAj~A z!nDCbwUTr9Ds4BDLo;PDh)4>`UDXDE`TML?G`j7;Jc<->^$x_VA+Xm?EE%!9fCo~3 zn`u|rHAXk!6oK+qnC$h3Rnq||JvL@w<&5L|@~J#ABV3RJA?4HTi9^A7C>BSiC*y^9 za)1K!S3|93Gxrmmq)5}?v&gqdB!OxwkH9Z*1({+n0-#qT13y$TgyIOlvGS&7B1QFU zn91smAn;NB3I;N;zp5|Vs8Xxkg%&*^5FB$?DwBTXMjWIg&}v)FRm83F;+>@$U1m)ts@zj*Ot27tDk@Go6A;|)Avj_-eIrP?wK4dM2|ulXj) z&0rvfVmSkIXVth9Z>((!z?crerTPbPNzx(7;5Ss;i)oxw)F)-PSPpak@C!v7tgB4v zUYPX9=qHC5!N^>MV>bm!V?g-h#K<14 z66Ol0L6cj_AK)#rO1sv=n@A68dW3h1es1EcYec#!s|(nL;Ro6|$@Do>EZpBPSu7mD-p(aeU{rs^5@?n!86|`-g(x$a z>mF_X6uo4^ZwX^clO(j(j-j)5p>hGIWkFHFX0cN07+npA8#|u(QqMZSsuCs3+@xbU zqPxR#?3>tTN+4V>-Ue}MfC;>!?(dm(1hc#Jt1dZk2;xFc7_5&oLSP8!ghbIktqW;B z(nN_QI-$9f9CUq@CAgK492U&a|2>Ga-4fRtT$qj{BAoNfowk8BarzK z4194SMo-9vJy$MUETE{NIrq!+p@S@n7~A-4zAG|p4~D#$vo6jE-R?jmB9I;ecQ}Q; z4it?UMU-Vni_=EvrVglY)dG?;%jqhw`|O%H)i2t(7s}bnAWcD7usTrcrCKYRakC!X zCIL@GpC7u|r?MUSo>WwfC?*>Nx&B-@Azrov2ur^L`U{p9SS`Om0eATlt*{)cRy%ZFVINA#UHR(!S=?edQN?(%)mNk1L7MgM9LQ>~k0t04BrlqOa-~ zs2!EylmC$e+Krh+^8^2``UkY1%qff#W_HjMCxPA#gN41T)OZ~tE zHAkXBeH}TC{YWS)agZAr^4&VbSD;pnFQVxYY^TCt={{rQU2s?29qx78)t^2}ReEPr z=ZjDeIwa^nl>3bTcPRP)i^9Oh`2SKEm>K_T$a{6#gpIN|Qpn2#+7}qF&Wp{MVLZ_d zfVdw3ZRoF$CMv|yF9xv9?HOh>orI>7!FX<)Tv=LjzBnI{UN`G(D&IG>mecNxw_~RS>AmZs$@uE$1o%Li^!x8=1i7@uuyisdaT5k_7Bck z-lLl)NwvvHGexI^j^9GL6w;-5^ew%bMVl1=c)&m`PqTLL01s*buO6U|lw&6VA@q{v zLk7RjDxEE#-bb^IysQj#0*O`)T1?$NyOP*1c@HwIUsj*!aUKptB(6ka{8ZU30V$-S zggYGW`;qk04>RlGz_bLQ&$9v*yvQ7uUpcK!ds#BU41(Mchy=#KzZ^k@6a%4kH9hU} zQF=vI6@%xw{oM2QE#9~UiDy-`mDl$Xd$ZlO&g*_J>te^=TG9{5>!{Aqj$XrGL6P3X zXVfw<_>AeKZJ1ZFv=ml%UhN(}KPMM$L)qHBN10g8wqz3)d!Z#Db5TI77&~h7P?x21 z$eU42x>%-2C?CvlI@E$zIhT$wbbnHb%qrHSvh%t7uE@@A{^E=vv$%gsb4hl~=oZSfu}c`|u;w z{c&Ht@(sZWeNjt=@+&;u`hMHH-#-Xt51lYd=I?$#x_z!);n(r9poi4uI2Wh&2{zaD z?NzsPI=r@u>sZT%Kc0g?E}V%r<1wkdLT7rvFRQ051E?B>D`2aJKQ*@x(381{A<~V* zUm%ON#f%nbXlKrk?BmctjTUs1Z_ha_sxQmoa@LO(NEIrT)f+ioev_%5T&`^=t=`i8 z`vB|7md`2&HAAnWxa$UyI1{KHma=pnAvroi(L>km7I$@>q09ZQ7O`y+8k=(xsZxjR z75iQ1?rH!+EwuWL9r_3^0xh)UbJ4q?E*d7s2}v#5!wXf#O+)PP*0~Vt`j9Ixt0w8p zs(f(jf0FO2lfLUrT2R10CdjCr4-olfo6SfImKvRTA8PbeRl~fwTQt1TzlX2%Q0MuK zrO`P3-wT-0KB<5XkS-cu-yWt11uAyLTXNMl|$0pOg$aVAt;^B*9;vDm;L*q`%~Sx_?BKFOVzAdSO66!#F>4C`2&;Fmp$w&&`}0iW>9G60 zvRw=9Ti?IAr8KhY^_xLH1T#+P$4JL-=^xQjANpT%D96TH6SmYmwu}?>bo@#5jNRjy zxv^82dQV<=Dl@6@%V?sQ* z_dj)0leG)v1leqN_~wmwqJ0lwfA8=m*m^`YUi!#VmWW^eDb8U<-^nrm!==peAHV=i z?5qs`?F{CBHbwr&tnn|gMs5h@m%CEhB+GTBQ|fAMb4`5aM2XT87gr$l+hw!o+oc{> zN||&bWns{rQ7{e+qoe2RDiqyAP51Q6?(XU$@}ySzI-Fs2K-;aBzKa8~U&4(_+cDX_ zmgXk(ml<0;a@G4&d@7d z89>jkQ%pY^%Cbc&bzK9s9*5zik+4)wt}K7a1W-<&DX3WfQnJCZwq{Z%26?9gR-)L} zE+dL@VB%co*jVSdhulK6x}He6(wZ$y?#}mg-S4Loe&dx8Zw(i?Z`f9TCtRMM7MRfs zXV_+)!5c&-f*>M~!;XAVjj+3EELYr_JYWdIS2(}W!21yW6QNQr88KDikcOACE@||X zw}*VtJ{V4hBlN2FNrtWTc7gl=WvaOV_r>m-OV(5>;qaEHbc%Q>cC;&E>C}_u`fgoT zknBQUZOR#BwqR<%gf*+Hk))+K^U=x+DhRZ znaKXof?FL739-G$YLnt|6vw{4wm4L<>^q^MpuB*H>Rd>Xv9zQ-b_r#tuVjrU5P^<6 z!-4;)oOIQXb`6)p+-*-!a$fO~EyR0@=4+$AF>KM8(ceQeUi}x4S=T}TRHM|?weZ=# zke4#PE8gA+cK@l8Ko0SjVY2(y?eKNIkRoOs80)MrE{$?Ec;*T@u*K}rQDY8cjj>n8-kpPH_ z{w;xsgBeM8FH4T3kYDB8kaftD^ZqCz2 z3v#rcey1gnfTbe!{~eRCe1*zBipZ>f!N4D>4{*2W65P>REZKDMZpqxCtO=^Rk_Ey8yQDRRZlbw3=dtwW_Y&~bn?Lg+uNfL}U z@UE#~6e)@{{Dfgw9Fd#1L1z(g(A1A$r4u2pW$ymd3Y>5B8Nyr>6<{|Jc?aMKgdW(2 zJWB;ucN9M6fBIT<9oHq2(BU|I7kH>8aoN(vvR~7|Irmj{D8tY}KT|EuQGAPLcEbHQ_tBi|N zbP0Jd78B~R0+0|=)S3b6oPw*5bK#j@ELmhyB3Wcki+rtr^7BmF747%W^u&eZvRCR{ zYp=4lHJ@oBolZ9xS;Uf9WCg}>$UKGY5p#NbX6{!(B}7~eZJZMYWzy`o17ho-a6W7o zeC?G=un#RXXXMH>zFAA9sc1LD`F$_D0Alo)MyY+CA=29?D81quPt}?WGgw>t7)uuZ z2$NxLhb>N5e+>p!#0L2GT{un!1uJe1_c^Z$H~QWHq^n-fs#L^D&r8$M4)2Y8(V46?VQ21#?JKpD8FI8I*fXT5#bBdeh_mH z7FEGAT2Gl=sYdEzU)((NqGdsAcj%gHW{!BZtc(5_iWbon+4yF`qi}s5k?$xPle|Ag zdfl2LM`%xM#lyz~(+%W`$XIp7T>oWexl^(b%ybo1igVILAtAT)Bj_H^_RX26&N`ro z_S*Q4G7w*lt#p`!LOdm|hQYrtNRSMJC3W4@h2eT#^C7Mb7($^{9TV7h7E%d4eUxZ2 zE!TB?6`Hj}Drtjm<9x;>y#3EXC*c{Fp)`p$hH3Lv=jA#rqB4POIZTqnIDZ~rmR)j` z&@T1$Tjy!K0g;Wv63E+9NTcOOS3j2JL73>4-0&}AM96=J^)XuJl~Jx!_HbXwfgiWC z`>=Lb#VWx1@n$q01g@I&0mu!@iOW%AWnF8zOI&V|OrEh(?y1$s?z-IrQNu5>R4Fc~C9h%eqRF*wftKHPWn(uD|BVi@L!NAwtLRQKHxf@o;1#L>|vpI)Xou50^b8E+|1}g0~AC zvUrCAxY%aGe!usT?e)a?#8y?i4>V2OHL7!=*8f<2j-oQ>lw#PgdgHJe*t)mp8R&h2 zlyQ2VZ2kBI5IAz=$+&!X(GZbG1}$VaP;DC(A_H!{y7MSQ*$>)2lCuanp-<^6`#2bo!%rJwS%j zTd$ORr0z(e|0pVOMbfV)j%rUtzER+Fgc&}N_u|F%t^A)+KB2efj)y!5MG@UOXznyO z>DWxPNK{W@U~i8gr4I+)oq0}9c2K?Xp z$4Y9ByRrsl-2_E8N4%)zCIv-;1*59R-k%bv?+ zv?HG&FFSRI_E265i*?^m@0aGt&Zp8#R@W0j#Q;7duWB*FFIAM|o3lu*DwyCUx;0YW z`3Hhlok*GhxmM1{mrFaFJ|OS{H2K!AQwkWwxPm7g0$&X?4Na!}H0-X4@wX-ROySJAyxj@TE#sd2}C zFw}T^zY+t@B4)*mK8#4lLe`fkz>ubnV$R5vUv{?2W{ z`3S8Fx{fOj|2cQ8wM;{Q8v@*C;_RT#W~yZVruXUjyxU(o8XAd&DuV4YqltnuB8z_C11|~CET&_S4@{i zwP(30Hn}FF{bHkvKI5HGOh%m?L1llBIh>OZJ#pUBwo_Qx);G->fsL%Kw4Ws`xPQu^ z2X25!E_ehqhq88Q`{~+-;A!dVk|Uh7yTF(foo^wGCZo$2%2Xaqj&uY4HI7w?hHq{E zJB0*;RGUD{|M*|CTo=aC)sKhV@w@I{lqYd8&K@cJ&`XDB3M01hCuY?iDFFOH8F)A? z5YLPRcqUAM%0XmvB|a9Zg=EK|TGPE+vj?t;@L`YQI_;LHF>K)ap$^)8f*At-ZGS~z zs<5XsGesaa&dLbm=6HtzCapn($}L`P4YI2fC&Hn5vuVm%@%*U0{86RSHr3><+#_Gs zH2Cu|w8YAz=LB)mNRPu)umZ-7@|$nXQhZn3lwMnqKyOdv*`pVxv~fjg5X;`PgzmWN zoTVc|1x2Ku6wh}>8CQTVzQRYxX|S;ZKO>ApH2Sl_cGJ@(K#+e*9ld(`qonI&-k3H< zCGN9&w-f!f-cFTh>u%Q>X1Y|(4Ja#@C?xQqt=)~l815I<{E75%%l+4o@g*|h_rLVC zMf@Mh;T(FiDtaIAMV|MG(ouCd`95nDW@W4TB$@Y#;wj1315(k#m2GkD)uN}BwLcl> zR-4E?62HH_4yIMRdpNFn_6PLHuVsGS(;iGefJZ-l*ZO6M`@ZRKhAZVgLqVZKX@SzE zdM|IE<`j&6ts_XVpq$dngo;814EmCA{XfTb!ctY(Q<^3xtm_9oy+s|w0(LKbP$>D-y@vT4`VC*6f(74*tv zb>Tz)OpO#iZHiEcn*yTcVso2RneAzHLFq|Fz9rO0d3*iW)$XC)y$Uv3<}Ne6?u41E z?@iuhlde6jG$KIB|Ln(^4w*<2~9>pi0(C3&>Udfp{lz zBQe0>eqKW4P)p5?;8GXyLwvx2VvKRcm); zH_g5tVMcneP};zJFs;_IMv(Hr0R(4ixZ+1pfEu4KnNlcP z0lL@5v~)6X85W~}pxK4xr%H6v;YsK7Ks)sHY;H4hnmny&1&l!%uDMtI*0r6cMw;|{cepPOl+#n_mp!!XShyxZUOZqoX*~`hTe+b} z28Llnop6}SArza>FEx7Cwr*d<5{E`nO4dYq4R0DCx;>h0S=y~^RJ*uscY#9Ovns2O zUYAv_`ArjETOvgnorRNvNQZ5~RS48o&5T2!W*Hvp%%;PCwVOv0usoXAABZ{1!p6VS z%T9yxi_A%CSg#q=Y-pi-*Ivb5w3Nq)nt+k%rpb{yE9P%d^iR`)+wFh_>>D?%A)y|R zViwV~?~jX!oid&D+;+~fVu3bz$@Qp-{_U%>#S%KN4Qv|{)}IEb`a59wn#=N7pj<|b z<{OzxD4V{v0A(Yv6_{ek_trRW96gp&DypO3D?SGYDXU}9>@sY@ao761C41bLOxw4t z3G_gUCX6Vdg*%e8AoT`id-z`cDuf|EG3ITSILYrR;XlrQVSIT;|4-!0w1;Y zc=(Gq3x)`MDS3nom+@%+E;uAHk|q_6QeSymwbOKcBW%`5BtBds*?0_V$-OG5*+k>= zED4&X5kd0%^3JypdzS9xXF11ZVOjY_vRBwkJBL(Tkp~q#B$#>T1$daPr{W1Pbey5@G~7ob%mV4ifa3DEeRq8`WfTt(|Chv%t! z7^b1Itcu|4XuV}Fa&iwih9%m7UaU)e>oR!51!00t#H9!rUTVQ)gZ!@f`L_x0Z;JMZ z0Cn|@4Z?|AOZfM=))uDkxo{FB%H(=|(j;+qjkdZs(4E?eYDiY8z|odQM# z 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ξE#B75prZqb?(=M|&~M}PWw`c6J1 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�e>VZZHf8)L6&s6|m*maF{8^XNkEc0ZP1WW$k+ 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�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. \\ +\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