feat(solver/normalization): Added normalization system

The dynamical range of different fields SERiF must solve for is massive. This just breaks any reasonable preconditioner / spreads the eigen modes of the system matrix out. This is a normalization system which uses charectaristic scale factors (e.g. the approximate radius estimated using the lane-emden equation) to rescale different fields to a more similar numeric scale. This turns out to be essential for stable convergence. Broadly this implements Riesz normalization along with exact diagnoal normalization.
This commit is contained in:
2026-11-09 13:30:20 -05:00
parent 9da779417b
commit 24d4c20f82
21 changed files with 1467 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
#pragma once
#include <array>
#include <cstddef>
#include <expected>
#include <span>
#include <string>
#include <tuple>
#include <type_traits>
#include "serif/solver/normalization/detail/factor_computations.hpp"
#include "serif/solver/normalization/detail/packing.hpp"
namespace serif::solver::normalization
{
namespace detail
{
/** @brief Test every coordinate factory without instantiating an unusable runtime graph. */
template <typename RulesT, typename LayoutT, typename ScalesT, discretization::blocks::RepresentedValueKind Kind, typename... Coordinates>
[[nodiscard]] consteval bool can_prepare_coordinates(utils::dependencies::KeyList<Coordinates...>)
{
return (requires
{
coordinate_computation<Kind, Coordinates, LayoutT, ScalesT>(utils::dependencies::bound_type_t<RulesT, Coordinates>{});
} && ...);
}
/** @brief Expand the supplied layout, looking up methods without imposing plan declaration order. */
template <typename RulesT, typename LayoutT, typename ScalesT, discretization::blocks::RepresentedValueKind Kind, typename... Coordinates>
[[nodiscard]] auto coordinate_computations(utils::dependencies::KeyList<Coordinates...>)
{
return std::tuple{coordinate_computation<Kind, Coordinates, LayoutT, ScalesT>(utils::dependencies::bound_type_t<RulesT, Coordinates>{})...};
}
/** @brief Freeze the independently ordered state and residual coordinates into one owning result. */
template <typename... Values, typename... Residuals>
[[nodiscard]] auto packing_computation(utils::dependencies::KeyList<Values...>, utils::dependencies::KeyList<Residuals...>)
{
using namespace utils::dependencies;
using discretization::blocks::RepresentedValueKind;
return make_computation<keys::Normalization, InputList<keys::Factors<RepresentedValueKind::value, Values>..., keys::Factors<RepresentedValueKind::residual, Residuals>...>>(
[](const auto&... factors) -> std::expected<DiagonalNormalization, std::string>
{
const std::array<std::span<const double>, sizeof...(Values) + sizeof...(Residuals)> coordinates{factors...};
const std::span<const std::span<const double>> blocks{coordinates};
return pack_normalization(blocks.first(sizeof...(Values)), blocks.subspan(sizeof...(Values)));
});
}
}
template <typename PlanT, typename LayoutT, typename ScalesT = StellarReferenceScales>
concept NormalizationPreparable = CoordinateLayoutType<LayoutT> &&
CompleteNormalizationFor<PlanT, typename std::remove_cvref_t<LayoutT>::Values, typename std::remove_cvref_t<LayoutT>::Residuals> &&
detail::can_prepare_coordinates<typename std::remove_cvref_t<PlanT>::Values, std::remove_cvref_t<LayoutT>, std::remove_cvref_t<ScalesT>, discretization::blocks::RepresentedValueKind::value>(typename std::remove_cvref_t<LayoutT>::Values{}) &&
detail::can_prepare_coordinates<typename std::remove_cvref_t<PlanT>::Residuals, std::remove_cvref_t<LayoutT>, std::remove_cvref_t<ScalesT>, discretization::blocks::RepresentedValueKind::residual>(typename std::remove_cvref_t<LayoutT>::Residuals{});
/**
* @brief Declare normalization as ordinary dependency computations; no metrics or factors are prepared here.
* @note Supply keys::Layout, keys::Scales, and the plan's metric/selection/prescription sources. Copy the accepted DiagonalNormalization to freeze one solve before preparing later candidates.
*/
template <typename PlanT, typename ScalesT = StellarReferenceScales, typename LayoutT>
requires NormalizationPreparable<PlanT, LayoutT, ScalesT>
[[nodiscard]] auto make_normalization_computations(const LayoutT&)
{
using discretization::blocks::RepresentedValueKind;
using Plan = std::remove_cvref_t<PlanT>;
using Layout = std::remove_cvref_t<LayoutT>;
using Scales = std::remove_cvref_t<ScalesT>;
return std::tuple_cat(
detail::coordinate_computations<typename Plan::Values, Layout, Scales, RepresentedValueKind::value>(typename Layout::Values{}),
detail::coordinate_computations<typename Plan::Residuals, Layout, Scales, RepresentedValueKind::residual>(typename Layout::Residuals{}),
std::tuple{detail::packing_computation(typename Layout::Values{}, typename Layout::Residuals{})});
}
}

View File

@@ -0,0 +1,95 @@
#pragma once
#include <concepts>
#include <expected>
#include <string>
#include <tuple>
#include <utility>
#include "serif/dimensions/type_alias.hpp"
#include "serif/solver/normalization/scales.hpp"
#include "serif/utils/dependencies/dependency_context.hpp"
#include "serif/utils/laneemden/laneemden.hpp"
namespace serif::solver::normalization {
/** @brief Obtain a reference value from the corresponding descriptive model prescription. */
struct FromModel final {};
/** @brief Derive the coordinate rules from the selected fields, equations and boundary formulation. */
struct AutomaticPlan final {};
/** @brief Remove physical and geometric units from the solver's coordinates and norms. */
struct Dimensionless final {};
/**
* @brief Estimate a reference radius from a Lane–Emden shape, mass and central density.
* @note These describe the numerical reference star; they never alter the model's EOS or constraints.
*/
template <typename Mass = FromModel, typename CentralDensity = FromModel>
requires (std::same_as<Mass, FromModel> || std::same_as<Mass, dimensions::MassValue>) &&
(std::same_as<CentralDensity, FromModel> || std::same_as<CentralDensity, dimensions::DensityValue>)
struct LaneEmdenApproximation final {
double n{3.0};
Mass mass;
CentralDensity central_density;
utils::laneemden::IntegrationOptions integration{};
};
/**
* @brief Declare normalization without evaluating a reference star or allocating coordinate factors.
* @note G uses the model's declared units; its default is the existing G=1 code-unit convention.
* Advanced users may supply a complete plan and ordinary graph sources/computations for custom rules.
*/
template <typename Scheme = LaneEmdenApproximation<>, typename Coordinates = Dimensionless,
typename Plan = AutomaticPlan, typename Sources = utils::dependencies::DependencyContext<>, typename Computations = std::tuple<>>
requires std::same_as<Coordinates, Dimensionless> && utils::dependencies::IsDependencyContext<Sources>
struct NormalizationOptions final {
Scheme scheme;
Coordinates coordinates{};
Plan plan;
dimensions::QuantityValue<dimensions::GravitationalConstant> gravitational_constant{1.0};
Sources sources;
Computations computations;
};
/** @brief Preserve the deduced numerical choices for later preparation with a model and discretization. */
template <typename Scheme, typename Coordinates, typename Plan, typename Sources, typename Computations>
[[nodiscard]] auto make_normalization(NormalizationOptions<Scheme, Coordinates, Plan, Sources, Computations> options) {
return options;
}
/** @brief Select the usual dimensionless Lane–Emden reference; supply its central density before resolving it against a model. */
[[nodiscard]] inline auto make_normalization() {
return NormalizationOptions<>{};
}
/** @brief Derive the shared stellar scales after both dimensional reference inputs have been resolved. */
[[nodiscard]] inline std::expected<StellarReferenceScales, std::string> prepare_reference_scales(
const LaneEmdenApproximation<dimensions::MassValue, dimensions::DensityValue>& scheme,
dimensions::QuantityValue<dimensions::GravitationalConstant> gravitational_constant) {
const auto solution = utils::laneemden::solve(scheme.n, scheme.integration);
if (!solution) {
return std::unexpected(solution.error());
}
const auto reference = utils::laneemden::scale_from_mass(*solution, scheme.mass, scheme.central_density, gravitational_constant);
if (!reference) {
return std::unexpected(reference.error());
}
return prepare_stellar_reference_scales(reference->mass, reference->radius, gravitational_constant);
}
/** @brief Use explicitly prepared reference scales through the same graph as estimated references. */
[[nodiscard]] inline std::expected<StellarReferenceScales, std::string> prepare_reference_scales(
const StellarReferenceScales& reference, dimensions::QuantityValue<dimensions::GravitationalConstant> gravitational_constant) {
if (reference.gravitational_constant != gravitational_constant) {
return std::unexpected("The explicit normalization reference and configuration use different gravitational constants. Supply one G in the model's units.");
}
return prepare_stellar_reference_scales(reference.mass, reference.radius, gravitational_constant);
}
/** @brief A custom stellar reference strategy supplies the same typed scales and ordinary failure value. */
template <typename Scheme>
concept ReferenceScheme = requires(const Scheme& scheme, dimensions::QuantityValue<dimensions::GravitationalConstant> gravity) {
{ prepare_reference_scales(scheme, gravity) } -> std::same_as<std::expected<StellarReferenceScales, std::string>>;
};
}

View File

@@ -0,0 +1,213 @@
#pragma once
#include <concepts>
#include <cstddef>
#include <expected>
#include <string>
#include <type_traits>
#include <vector>
#include "serif/solver/normalization/keys.hpp"
#include "serif/solver/normalization/layout.hpp"
#include "serif/solver/normalization/plan.hpp"
#include "serif/solver/normalization/diagonal.hpp"
#include "serif/solver/normalization/detail/validation.hpp"
#include "serif/solver/normalization/point_rows.hpp"
#include "serif/solver/normalization/scales.hpp"
#include "serif/utils/dependencies/computation.hpp"
namespace serif::solver::normalization::detail
{
template <typename ScalesT, typename QuantityT>
concept ProvidesScale = requires(const ScalesT& scales)
{
{ characteristic_scale(scales, QuantityT{}) } -> std::same_as<dimensions::QuantityValue<QuantityT>>;
};
/** @brief Reject a layout that changes the arity of a declared global scalar. */
template <discretization::blocks::RepresentedValueKind Kind, typename CoordinateT, CoordinateLayoutType LayoutT>
[[nodiscard]] std::expected<std::size_t, std::string> coordinate_size(const LayoutT& layout)
{
const std::size_t size = layout.template size<Kind, CoordinateT>();
using Term = typename decltype(metric_term<CoordinateT>())::type;
return validate_coordinate_size(size, discretization::blocks::GlobalScalarTerm<Term>);
}
/** @brief Check supplied metrics or custom factors before publishing a coordinate result. */
template <discretization::blocks::RepresentedValueKind Kind, typename CoordinateT, CoordinateLayoutType LayoutT>
[[nodiscard]] std::expected<void, std::string> check_coordinate_size(const LayoutT& layout, std::size_t received)
{
const auto size = coordinate_size<Kind, CoordinateT>(layout);
if (!size)
{
return std::unexpected(size.error());
}
return check_coordinate_size(*size, received);
}
/** @brief Identity preparation requires a layout but no scale or metric sources. */
template <discretization::blocks::RepresentedValueKind Kind, typename CoordinateT, typename LayoutT, typename ScalesT>
[[nodiscard]] auto coordinate_computation(IdentityCoordinate)
{
using namespace utils::dependencies;
return make_computation<keys::Factors<Kind, CoordinateT>, InputList<keys::Layout>>(
[](const LayoutT& layout) -> std::expected<std::vector<double>, std::string>
{
const auto size = coordinate_size<Kind, CoordinateT>(layout);
if (!size)
{
return std::unexpected(size.error());
}
return std::vector<double>(*size, 1.0);
});
}
/** @brief Algebraic coordinates use a scalar metric without allocating an intermediate vector of ones. */
template <discretization::blocks::RepresentedValueKind Kind, typename CoordinateT, typename LayoutT, typename ScalesT, typename QuantityT>
requires ProvidesScale<ScalesT, QuantityT>
[[nodiscard]] auto coordinate_computation(PhysicalRieszCoordinate<QuantityT, UnitMetric>)
{
using namespace utils::dependencies;
return make_computation<keys::Factors<Kind, CoordinateT>, InputList<keys::Layout, keys::Scales>>(
[](const LayoutT& layout, const ScalesT& scales) -> std::expected<std::vector<double>, std::string>
{
const auto size = coordinate_size<Kind, CoordinateT>(layout);
if (!size)
{
return std::unexpected(size.error());
}
const auto factor = make_riesz_factor(Kind, 1.0, characteristic_scale(scales, QuantityT{}).value());
if (!factor)
{
return std::unexpected(factor.error());
}
return std::vector<double>(*size, *factor);
});
}
/** @brief Both coordinate roles consume the same cached reference metric with different primal/dual formulas. */
template <discretization::blocks::RepresentedValueKind Kind, typename CoordinateT, typename LayoutT, typename ScalesT, typename QuantityT, typename MetricT>
requires (!std::same_as<MetricT, UnitMetric> && ProvidesScale<ScalesT, QuantityT>)
[[nodiscard]] auto coordinate_computation(PhysicalRieszCoordinate<QuantityT, MetricT>)
{
using namespace utils::dependencies;
return make_computation<keys::Factors<Kind, CoordinateT>, InputList<keys::Layout, keys::Scales, MetricT>>(
[](const LayoutT& layout, const ScalesT& scales, const std::vector<double>& metric) -> std::expected<std::vector<double>, std::string>
{
const auto valid = check_coordinate_size<Kind, CoordinateT>(layout, metric.size());
if (!valid)
{
return std::unexpected(valid.error());
}
return make_riesz_factors(Kind, metric, characteristic_scale(scales, QuantityT{}).value());
});
}
/** @brief Read the replacement's quantity independently of the bulk equation's scale. */
template <discretization::blocks::RepresentedValueKind Kind, typename CoordinateT, typename LayoutT, typename ScalesT, typename QuantityT, typename MetricT, typename PointQuantityT, typename SelectionT>
requires (Kind == discretization::blocks::RepresentedValueKind::residual && !std::same_as<MetricT, UnitMetric> && ProvidesScale<ScalesT, QuantityT> && ProvidesScale<ScalesT, PointQuantityT>)
[[nodiscard]] auto coordinate_computation(WithPointRows<PhysicalRieszCoordinate<QuantityT, MetricT>, PointQuantityT, SelectionT>)
{
using namespace utils::dependencies;
return make_computation<keys::Factors<Kind, CoordinateT>, InputList<keys::Layout, keys::Scales, MetricT, SelectionT>>(
[](const LayoutT& layout, const ScalesT& scales, const std::vector<double>& metric, const std::vector<int>& rows) -> std::expected<std::vector<double>, std::string>
{
const auto valid = check_coordinate_size<Kind, CoordinateT>(layout, metric.size());
if (!valid)
{
return std::unexpected(valid.error());
}
return make_point_replaced_residual_factors(metric, characteristic_scale(scales, QuantityT{}).value(), rows, characteristic_scale(scales, PointQuantityT{}).value());
});
}
/** @brief Global scalars have no geometric measure to remove. */
template <discretization::blocks::RepresentedValueKind Kind, typename CoordinateT, typename LayoutT, typename ScalesT, typename QuantityT>
requires ProvidesScale<ScalesT, QuantityT>
[[nodiscard]] auto coordinate_computation(DimensionlessRieszCoordinate<QuantityT, UnitMetric, ReferenceGeometry::algebraic>) {
return coordinate_computation<Kind, CoordinateT, LayoutT, ScalesT>(PhysicalRieszCoordinate<QuantityT, UnitMetric>{});
}
/** @brief Cancel the reference measure while retaining the native finite-element coordinate representation. */
template <discretization::blocks::RepresentedValueKind Kind, typename CoordinateT, typename LayoutT, typename ScalesT, typename QuantityT, typename MetricT, ReferenceGeometry Geometry>
requires (!std::same_as<MetricT, UnitMetric> && ProvidesScale<ScalesT, QuantityT> && ProvidesScale<ScalesT, dimensions::Length>)
[[nodiscard]] auto coordinate_computation(DimensionlessRieszCoordinate<QuantityT, MetricT, Geometry>) {
using namespace utils::dependencies;
return make_computation<keys::Factors<Kind, CoordinateT>, InputList<keys::Layout, keys::Scales, MetricT>>(
[](const LayoutT& layout, const ScalesT& scales, const std::vector<double>& metric) -> std::expected<std::vector<double>, std::string> {
const auto valid = check_coordinate_size<Kind, CoordinateT>(layout, metric.size());
if (!valid) {
return std::unexpected(valid.error());
}
const auto scale = dimensionless_riesz_scale(Kind, Geometry, characteristic_scale(scales, QuantityT{}).value(), characteristic_scale(scales, dimensions::Length{}).value());
if (!scale) {
return std::unexpected(scale.error());
}
return make_riesz_factors(Kind, metric, *scale);
});
}
/** @brief Remove geometric units only from weak rows, leaving each strong point equation in its own physical units. */
template <discretization::blocks::RepresentedValueKind Kind, typename CoordinateT, typename LayoutT, typename ScalesT, typename QuantityT, typename MetricT, ReferenceGeometry Geometry, typename PointQuantityT, typename SelectionT>
requires (Kind == discretization::blocks::RepresentedValueKind::residual && !std::same_as<MetricT, UnitMetric> && ProvidesScale<ScalesT, QuantityT> && ProvidesScale<ScalesT, PointQuantityT> && ProvidesScale<ScalesT, dimensions::Length>)
[[nodiscard]] auto coordinate_computation(WithPointRows<DimensionlessRieszCoordinate<QuantityT, MetricT, Geometry>, PointQuantityT, SelectionT>) {
using namespace utils::dependencies;
return make_computation<keys::Factors<Kind, CoordinateT>, InputList<keys::Layout, keys::Scales, MetricT, SelectionT>>(
[](const LayoutT& layout, const ScalesT& scales, const std::vector<double>& metric, const std::vector<int>& rows) -> std::expected<std::vector<double>, std::string> {
const auto valid = check_coordinate_size<Kind, CoordinateT>(layout, metric.size());
if (!valid) {
return std::unexpected(valid.error());
}
const auto scale = dimensionless_riesz_scale(Kind, Geometry, characteristic_scale(scales, QuantityT{}).value(), characteristic_scale(scales, dimensions::Length{}).value());
if (!scale) {
return std::unexpected(scale.error());
}
return make_point_replaced_residual_factors(metric, *scale, rows, characteristic_scale(scales, PointQuantityT{}).value());
});
}
/** @brief Invoke only the exact declared prescription, supplied as a tracked source rather than a hidden mutable capture. */
template <discretization::blocks::RepresentedValueKind Kind, typename CoordinateT, typename LayoutT, typename ScalesT, typename PrescriptionT>
requires requires(const std::remove_cvref_t<PrescriptionT>& prescription, std::size_t size)
{
{ prescription.prepare_factors(keys::Factors<Kind, CoordinateT>{}, size) } -> std::same_as<std::expected<std::vector<double>, std::string>>;
}
[[nodiscard]] auto coordinate_computation(RuntimePreparedCoordinate<PrescriptionT>)
{
using namespace utils::dependencies;
using Prescription = std::remove_cvref_t<PrescriptionT>;
return make_computation<keys::Factors<Kind, CoordinateT>, InputList<keys::Layout, keys::Prescription<Prescription>>>(
[]<typename SuppliedPrescription>(const LayoutT& layout, const SuppliedPrescription& prescription) -> std::expected<std::vector<double>, std::string>
requires std::same_as<std::remove_cvref_t<SuppliedPrescription>, Prescription>
{
const auto size = coordinate_size<Kind, CoordinateT>(layout);
if (!size)
{
return std::unexpected(size.error());
}
auto factors = prescription.prepare_factors(keys::Factors<Kind, CoordinateT>{}, *size);
if (!factors)
{
return factors;
}
const auto valid_size = check_coordinate_size(*size, factors->size());
if (!valid_size)
{
return std::unexpected(valid_size.error());
}
const auto valid_factors = validate_factors(*factors);
if (!valid_factors)
{
return std::unexpected(valid_factors.error());
}
return factors;
});
}
}

View File

@@ -0,0 +1,13 @@
#pragma once
#include <expected>
#include <span>
#include <string>
#include "serif/solver/normalization/diagonal.hpp"
namespace serif::solver::normalization::detail
{
/** @brief Freeze independently ordered state and residual blocks into their packed solver coordinates. */
[[nodiscard]] std::expected<DiagonalNormalization, std::string> pack_normalization(std::span<const std::span<const double>> states, std::span<const std::span<const double>> residuals);
}

View File

@@ -0,0 +1,18 @@
#pragma once
#include <cstddef>
#include <expected>
#include <span>
#include <string>
namespace serif::solver::normalization::detail
{
/** @brief Require finite positive factors before accepting custom or packed normalization. */
[[nodiscard]] std::expected<void, std::string> validate_factors(std::span<const double> factors);
/** @brief Check representable coordinate storage and the fixed arity of global scalar terms. */
[[nodiscard]] std::expected<std::size_t, std::string> validate_coordinate_size(std::size_t size, bool global_scalar);
/** @brief Compare metric or factor size with the independently declared coordinate layout. */
[[nodiscard]] std::expected<void, std::string> check_coordinate_size(std::size_t expected, std::size_t received);
}

View File

@@ -0,0 +1,86 @@
#pragma once
#include <cstddef>
#include <expected>
#include <span>
#include <string>
#include <vector>
#include "serif/discretization/blocks/base.hpp"
namespace serif::solver::normalization
{
namespace detail
{
/** @brief Require matching coordinate sizes before any output is modified. */
void require_normalization_size(std::size_t received, std::size_t expected);
/** @brief Apply a diagonal in place or to disjoint storage, rejecting partial overlap. */
void apply_normalization(std::span<const double> factors, std::span<const double> input, std::span<double> output, bool inverse);
}
/** @brief Prepare one primal sqrt(metric)/scale or dual 1/(scale sqrt(metric)) factor. */
[[nodiscard]] std::expected<double, std::string> make_riesz_factor(discretization::blocks::RepresentedValueKind kind, double metric, double physical_scale);
/** @brief Prepare diagonal Riesz factors, reporting which coordinate has invalid data. */
[[nodiscard]] std::expected<std::vector<double>, std::string> make_riesz_factors(discretization::blocks::RepresentedValueKind kind, std::span<const double> metric, double physical_scale);
/** @brief Own the frozen, independent state and residual transformations used by one solve. */
class DiagonalNormalization final
{
public:
/** @brief Read the factors taking physical state coefficients to normalized coordinates. */
[[nodiscard]] std::span<const double> state_factors() const noexcept
{
return m_state_factors;
}
/** @brief Read the factors taking physical residual coefficients to normalized coordinates. */
[[nodiscard]] std::span<const double> residual_factors() const noexcept
{
return m_residual_factors;
}
/** @brief Number of state coordinates. */
[[nodiscard]] std::size_t state_size() const noexcept
{
return m_state_factors.size();
}
/** @brief Number of residual coordinates, which need not equal the state size. */
[[nodiscard]] std::size_t residual_size() const noexcept
{
return m_residual_factors.size();
}
/** @brief Convert physical state coefficients to normalized coordinates. */
void normalize_state(std::span<const double> physical, std::span<double> normalized) const;
/** @brief Recover physical state coefficients from normalized coordinates. */
void denormalize_state(std::span<const double> normalized, std::span<double> physical) const;
/** @brief Convert physical residual coefficients to normalized coordinates. */
void normalize_residual(std::span<const double> physical, std::span<double> normalized) const;
/** @brief Recover physical residual coefficients from normalized coordinates. */
void denormalize_residual(std::span<const double> normalized, std::span<double> physical) const;
/** @brief Squared primal norm of a physical state vector. */
[[nodiscard]] double state_norm_squared(std::span<const double> physical) const;
/** @brief Squared dual norm of a physical residual vector. */
[[nodiscard]] double residual_norm_squared(std::span<const double> physical) const;
private:
friend std::expected<DiagonalNormalization, std::string> make_diagonal_normalization(std::vector<double>, std::vector<double>);
/** @brief Accept factors only after the factory has validated them. */
DiagonalNormalization(std::vector<double> state, std::vector<double> residual);
std::vector<double> m_state_factors;
std::vector<double> m_residual_factors;
};
/** @brief Validate and freeze independent coordinate factors, including empty or rectangular systems. */
[[nodiscard]] std::expected<DiagonalNormalization, std::string> make_diagonal_normalization(std::vector<double> state, std::vector<double> residual);
}

View File

@@ -0,0 +1,43 @@
#pragma once
#include <expected>
#include <string>
#include "serif/solver/normalization/metrics.hpp"
namespace serif::solver::normalization {
/** @brief Identify the geometric convention of a reference coordinate metric. */
enum class ReferenceGeometry {
volume,
surface,
algebraic
};
/** @brief Require custom metrics to declare their geometric convention explicitly. */
template <typename MetricT>
struct DimensionlessMetricGeometry;
template <discretization::blocks::SpatialTerm TermT>
struct DimensionlessMetricGeometry<ReferenceVolumeMetric<TermT>> {
static constexpr ReferenceGeometry value = ReferenceGeometry::volume;
};
template <>
struct DimensionlessMetricGeometry<geometry::deformation::keys::SurfaceMetric> {
static constexpr ReferenceGeometry value = ReferenceGeometry::surface;
};
template <>
struct DimensionlessMetricGeometry<UnitMetric> {
static constexpr ReferenceGeometry value = ReferenceGeometry::algebraic;
};
namespace detail {
/**
* @brief Include reference geometry in the scale consumed by the existing Riesz factor calculation.
* @note Volume residual quantities are densities before weak integration; surface residual quantities are integrated forces.
*/
[[nodiscard]] std::expected<double, std::string> dimensionless_riesz_scale(
discretization::blocks::RepresentedValueKind kind, ReferenceGeometry geometry, double physical_scale, double reference_radius);
}
}

View File

@@ -0,0 +1,37 @@
#pragma once
#include "serif/discretization/blocks/base.hpp"
#include "serif/discretization/blocks/keys.hpp"
namespace serif::solver::normalization::keys
{
/** @brief Normalization consumes the same coordinate layout as every other numerical service. */
using Layout = discretization::blocks::keys::Layout;
struct Scales
{
};
struct Normalization
{
};
/** @brief Distinguish state and residual factors even when they share the same physical term. */
template <discretization::blocks::RepresentedValueKind Kind, typename CoordinateT>
struct Factors
{
using Coordinate = CoordinateT;
static constexpr discretization::blocks::RepresentedValueKind kind = Kind;
};
/** @brief Bind the exact custom prescription named by a runtime-prepared coordinate. */
template <typename PrescriptionT>
struct Prescription
{
};
template <typename TermT>
struct ReferenceBinding
{
};
}

View File

@@ -0,0 +1,14 @@
#pragma once
#include "serif/discretization/blocks/system_layout.hpp"
namespace serif::solver::normalization
{
/** @brief Compatibility spelling for the shared discretization layout. */
template <utils::dependencies::IsKeyList Values, utils::dependencies::IsKeyList Residuals>
using CoordinateLayout = discretization::blocks::SystemLayout<Values, Residuals>;
/** @brief Normalization consumes the same coordinate representation as physical operators and solvers. */
template <typename Candidate>
concept CoordinateLayoutType = discretization::blocks::SystemLayoutType<Candidate>;
}

View File

@@ -0,0 +1,85 @@
#pragma once
#include <concepts>
#include <type_traits>
#include "serif/discretization/blocks/concepts.hpp"
#include "serif/utils/dependencies/key_list_operations.hpp"
namespace serif::geometry::deformation::keys
{
struct SurfaceMetric;
}
namespace serif::solver::normalization
{
/** @brief A unit Gram metric for algebraic coordinates, still subject to physical scaling. */
struct UnitMetric final
{
};
/** @brief Identify a reference volume metric by its physical term, including its support and FE representation. */
template <discretization::blocks::SpatialTerm TermT>
struct ReferenceVolumeMetric final
{
using Coordinates = utils::dependencies::KeyList<TermT>;
};
/** @brief Metric keys advertise the coordinate identities they measure; a same-sized unrelated metric is insufficient. */
template <typename MetricT, typename = void>
struct MetricCoordinates
{
using Type = utils::dependencies::KeyList<>;
};
template <typename MetricT>
struct MetricCoordinates<MetricT, std::void_t<typename MetricT::Coordinates>>
{
using Type = typename MetricT::Coordinates;
};
/** @brief Permit a Euclidean norm of algebraic surface moments after their measure is made dimensionless. */
template <>
struct MetricCoordinates<UnitMetric>
{
using Type = utils::dependencies::KeyList<discretization::blocks::surface_deformation::shape_equilibrium>;
};
/** @brief Consume deformation's existing metric directly in both native shape-coordinate roles. */
template <>
struct MetricCoordinates<geometry::deformation::keys::SurfaceMetric>
{
using Type = utils::dependencies::KeyList<discretization::blocks::surface_deformation::coefficients, discretization::blocks::surface_deformation::shape_equilibrium>;
};
namespace detail
{
/** @brief Resolve assembled row identity without confusing a point-border row with its sampled field. */
template <typename CoordinateT>
[[nodiscard]] consteval auto metric_term()
{
if constexpr (requires
{
typename CoordinateT::Border;
})
{
return std::type_identity<typename CoordinateT::Border>{};
}
else if constexpr (requires
{
typename CoordinateT::Test;
})
{
return std::type_identity<typename CoordinateT::Test>{};
}
else
{
return std::type_identity<CoordinateT>{};
}
}
}
template <typename MetricT, typename CoordinateT>
concept MetricFor = (std::same_as<MetricT, UnitMetric> && discretization::blocks::GlobalScalarTerm<typename decltype(detail::metric_term<CoordinateT>())::type>) ||
utils::dependencies::ContainsKey<typename decltype(detail::metric_term<CoordinateT>())::type, typename MetricCoordinates<MetricT>::Type>;
}

View File

@@ -0,0 +1,10 @@
#pragma once
#include "serif/solver/normalization/plan.hpp"
#include "serif/solver/normalization/physical_coordinates.hpp"
#include "serif/solver/normalization/layout.hpp"
#include "serif/solver/normalization/scales.hpp"
#include "serif/solver/normalization/diagonal.hpp"
#include "serif/solver/normalization/computations.hpp"
#include "serif/solver/normalization/configuration.hpp"

View File

@@ -0,0 +1,44 @@
#pragma once
#include <concepts>
#include <type_traits>
#include "serif/solver/normalization/plan.hpp"
namespace serif::solver::normalization
{
namespace detail
{
/** @brief Select ordinary FE or algebraic metrics from the existing physical representation. */
template <typename CoordinateT>
[[nodiscard]] consteval auto default_metric()
{
using Term = typename decltype(metric_term<CoordinateT>())::type;
if constexpr (discretization::blocks::SpatialTerm<Term>)
{
return std::type_identity<ReferenceVolumeMetric<Term>>{};
}
else if constexpr (discretization::blocks::GlobalScalarTerm<Term>)
{
return std::type_identity<UnitMetric>{};
}
else
{
return std::type_identity<void>{};
}
}
}
/** @brief Describe an unknown from its existing quantity, support, and functional-space metadata. */
template <discretization::blocks::HasQuantity TermT>
requires (!std::same_as<typename decltype(detail::default_metric<TermT>())::type, void>)
using PhysicalValueRule = utils::dependencies::Binding<TermT, PhysicalRieszCoordinate<typename TermT::Quantity, typename decltype(detail::default_metric<TermT>())::type>>;
/** @brief Describe a residual using the equation's declared quantity, which may differ from the unknown it closes. */
template <typename EquationT>
requires requires
{
typename EquationT::ResidualQuantity;
} && (!std::same_as<typename decltype(detail::default_metric<EquationT>())::type, void>)
using PhysicalResidualRule = utils::dependencies::Binding<EquationT, PhysicalRieszCoordinate<typename EquationT::ResidualQuantity, typename decltype(detail::default_metric<EquationT>())::type>>;
}

View File

@@ -0,0 +1,170 @@
#pragma once
#include <concepts>
#include <type_traits>
#include "serif/dimensions/quantities.hpp"
#include "serif/discretization/blocks/base.hpp"
#include "serif/solver/normalization/metrics.hpp"
#include "serif/solver/normalization/dimensionless.hpp"
#include "serif/utils/dependencies/binding.hpp"
namespace serif::solver::normalization
{
struct NormalizationPrescriptionTag
{
};
template <typename Candidate>
concept NormalizationPrescription = std::derived_from<std::remove_cvref_t<Candidate>, NormalizationPrescriptionTag>;
/** @brief Leave the declared coordinates unchanged, including their physical units. */
struct IdentityCoordinate final
{
};
/** @brief Measure a physical quantity with the named reference Gram diagonal. */
template <dimensions::PhysicalQuantityType QuantityT, typename MetricKeyT>
struct PhysicalRieszCoordinate final
{
using Quantity = QuantityT;
using Metric = MetricKeyT;
};
/**
* @brief Measure coordinates using dimensionless physical fields and reference geometry.
* @note The surface convention applies to shape lengths and their integrated force residuals. Custom metrics must select their convention.
*/
template <dimensions::PhysicalQuantityType QuantityT, typename MetricKeyT, ReferenceGeometry Geometry = DimensionlessMetricGeometry<MetricKeyT>::value>
struct DimensionlessRieszCoordinate final {
using Quantity = QuantityT;
using Metric = MetricKeyT;
static constexpr ReferenceGeometry geometry = Geometry;
};
/** @brief Replace selected volume residual rows by point residuals with their own physical quantity. */
template <typename VolumeMethodT, dimensions::PhysicalQuantityType PointQuantityT, typename SelectionKeyT>
struct WithPointRows final
{
using Selection = SelectionKeyT;
};
/** @brief Identify the exact prescription supplying positive factors; this never claims identity scaling. */
template <NormalizationPrescription PrescriptionT>
struct RuntimePreparedCoordinate final
{
using Prescription = std::remove_cvref_t<PrescriptionT>;
};
/** @brief Independent term-to-method and row-to-method maps; their order does not define the packed layout. */
template <typename ValueRulesT, typename ResidualRulesT>
struct NormalizationPlan final
{
using Values = ValueRulesT;
using Residuals = ResidualRulesT;
};
namespace detail
{
template <typename RulesT>
struct RuleKeys;
template <typename... Rules>
struct RuleKeys<utils::dependencies::Bindings<Rules...>>
{
using Type = utils::dependencies::KeyList<typename Rules::Key...>;
};
/** @brief Compare memberships in both directions without deduplicating ownership declarations. */
template <typename... Declared, typename... Required>
[[nodiscard]] consteval bool exact_coordinates(utils::dependencies::KeyList<Declared...>, utils::dependencies::KeyList<Required...>)
{
return sizeof...(Declared) == sizeof...(Required) && (utils::dependencies::ContainsKey<Required, utils::dependencies::KeyList<Declared...>> && ...);
}
/** @brief Check physics metadata when present; explicit low-level residual declarations remain their own authority. */
template <typename KeyT, typename QuantityT, discretization::blocks::RepresentedValueKind Kind>
[[nodiscard]] consteval bool quantity_matches()
{
if constexpr (Kind == discretization::blocks::RepresentedValueKind::value && requires
{
typename KeyT::Quantity;
})
{
return std::same_as<QuantityT, typename KeyT::Quantity>;
}
else if constexpr (Kind == discretization::blocks::RepresentedValueKind::residual && requires
{
typename KeyT::ResidualQuantity;
})
{
return std::same_as<QuantityT, typename KeyT::ResidualQuantity>;
}
else
{
return true;
}
}
template <typename MethodT, typename KeyT, discretization::blocks::RepresentedValueKind Kind>
struct MethodSupports : std::false_type
{
};
template <typename KeyT, discretization::blocks::RepresentedValueKind Kind>
struct MethodSupports<IdentityCoordinate, KeyT, Kind> : std::true_type
{
};
template <typename QuantityT, typename MetricT, typename KeyT, discretization::blocks::RepresentedValueKind Kind>
struct MethodSupports<PhysicalRieszCoordinate<QuantityT, MetricT>, KeyT, Kind> : std::bool_constant<MetricFor<MetricT, KeyT> && quantity_matches<KeyT, QuantityT, Kind>()>
{
};
template <typename QuantityT, typename MetricT, typename PointQuantityT, typename SelectionT, typename KeyT>
struct MethodSupports<WithPointRows<PhysicalRieszCoordinate<QuantityT, MetricT>, PointQuantityT, SelectionT>, KeyT, discretization::blocks::RepresentedValueKind::residual> : MethodSupports<PhysicalRieszCoordinate<QuantityT, MetricT>, KeyT, discretization::blocks::RepresentedValueKind::residual>
{
};
template <typename QuantityT, typename MetricT, ReferenceGeometry Geometry, typename KeyT, discretization::blocks::RepresentedValueKind Kind>
struct MethodSupports<DimensionlessRieszCoordinate<QuantityT, MetricT, Geometry>, KeyT, Kind> : std::bool_constant<
MetricFor<MetricT, KeyT> && quantity_matches<KeyT, QuantityT, Kind>() &&
(Geometry == ReferenceGeometry::volume || Geometry == ReferenceGeometry::surface || Geometry == ReferenceGeometry::algebraic) &&
(!std::same_as<MetricT, UnitMetric> || Geometry == ReferenceGeometry::algebraic)> {
};
template <typename QuantityT, typename MetricT, ReferenceGeometry Geometry, typename PointQuantityT, typename SelectionT, typename KeyT>
struct MethodSupports<WithPointRows<DimensionlessRieszCoordinate<QuantityT, MetricT, Geometry>, PointQuantityT, SelectionT>, KeyT, discretization::blocks::RepresentedValueKind::residual> :
MethodSupports<DimensionlessRieszCoordinate<QuantityT, MetricT, Geometry>, KeyT, discretization::blocks::RepresentedValueKind::residual> {
};
template <typename PrescriptionT, typename KeyT, discretization::blocks::RepresentedValueKind Kind>
struct MethodSupports<RuntimePreparedCoordinate<PrescriptionT>, KeyT, Kind> : std::true_type
{
};
template <typename RulesT, discretization::blocks::RepresentedValueKind Kind>
struct ValidRules : std::false_type
{
};
template <typename... Rules, discretization::blocks::RepresentedValueKind Kind>
struct ValidRules<utils::dependencies::Bindings<Rules...>, Kind> : std::bool_constant<utils::dependencies::IsBindings<utils::dependencies::Bindings<Rules...>> && (MethodSupports<typename Rules::Target, typename Rules::Key, Kind>::value && ...)>
{
};
template <typename Candidate>
inline constexpr bool valid_plan = false;
template <typename ValuesT, typename ResidualsT>
inline constexpr bool valid_plan<NormalizationPlan<ValuesT, ResidualsT>> = ValidRules<ValuesT, discretization::blocks::RepresentedValueKind::value>::value && ValidRules<ResidualsT, discretization::blocks::RepresentedValueKind::residual>::value;
}
template <typename Candidate>
concept NormalizationPlanType = detail::valid_plan<std::remove_cvref_t<Candidate>>;
template <typename PlanT, typename ValuesT, typename ResidualsT>
concept CompleteNormalizationFor = NormalizationPlanType<PlanT> && utils::dependencies::IsKeyList<ValuesT> && utils::dependencies::IsKeyList<ResidualsT> &&
detail::exact_coordinates(typename detail::RuleKeys<typename std::remove_cvref_t<PlanT>::Values>::Type{}, std::remove_cvref_t<ValuesT>{}) &&
detail::exact_coordinates(typename detail::RuleKeys<typename std::remove_cvref_t<PlanT>::Residuals>::Type{}, std::remove_cvref_t<ResidualsT>{});
}

View File

@@ -0,0 +1,15 @@
#pragma once
#include <expected>
#include <span>
#include <string>
#include <vector>
namespace serif::solver::normalization
{
/**
* @brief Measure volume and replacement point residuals in their independently specified physical units.
* @note Only replaced rows may have a zero or otherwise unusable volume metric. No bulk factor is evaluated for them.
*/
[[nodiscard]] std::expected<std::vector<double>, std::string> make_point_replaced_residual_factors(std::span<const double> volume_metric, double volume_scale, std::span<const int> selected_rows, double point_scale);
}

View File

@@ -0,0 +1,115 @@
#pragma once
#include <cmath>
#include <concepts>
#include <expected>
#include <string>
#include "serif/dimensions/dimensions.hpp"
namespace serif::solver::normalization
{
/** @brief Fixed reference scales used throughout a solve, independent of its candidate state. */
struct StellarReferenceScales final
{
dimensions::QuantityValue<dimensions::Mass> mass;
dimensions::QuantityValue<dimensions::Length> radius;
dimensions::QuantityValue<dimensions::GravitationalConstant> gravitational_constant;
dimensions::QuantityValue<dimensions::MassDensity> density;
dimensions::QuantityValue<dimensions::Acceleration> acceleration;
dimensions::QuantityValue<dimensions::InverseTimeSquared> inverse_time_squared;
dimensions::QuantityValue<dimensions::SpecificEnergy> specific_energy;
dimensions::QuantityValue<dimensions::Pressure> pressure;
dimensions::QuantityValue<dimensions::AngularVelocity> angular_velocity;
dimensions::QuantityValue<dimensions::AngularMomentum> angular_momentum;
dimensions::QuantityValue<dimensions::Force> force;
/** @brief Compare reference values for dependency snapshot invalidation. */
friend bool operator==(const StellarReferenceScales &, const StellarReferenceScales &) = default;
};
/** @brief Derive gravitational stellar scales from a finite, positive reference mass, radius, and G. */
[[nodiscard]] std::expected<StellarReferenceScales, std::string> prepare_stellar_reference_scales(
dimensions::QuantityValue<dimensions::Mass> mass,
dimensions::QuantityValue<dimensions::Length> radius,
dimensions::QuantityValue<dimensions::GravitationalConstant> gravitational_constant = dimensions::QuantityValue<dimensions::GravitationalConstant>{1.0});
/** @brief Quantities whose characteristic scales follow from reference mass, radius, and G alone. */
template <typename Quantity>
concept StellarScaleQuantity = std::same_as<Quantity, dimensions::Dimensionless> || std::same_as<Quantity, dimensions::Mass> ||
std::same_as<Quantity, dimensions::Length> || std::same_as<Quantity, dimensions::MassDensity> ||
std::same_as<Quantity, dimensions::Acceleration> || std::same_as<Quantity, dimensions::InverseTimeSquared> ||
std::same_as<Quantity, dimensions::SpecificEnergy> || std::same_as<Quantity, dimensions::SpecificEnthalpy> ||
std::same_as<Quantity, dimensions::BarotropicPressurePotential> ||
std::same_as<Quantity, dimensions::GravitationalPotential> || std::same_as<Quantity, dimensions::Pressure> ||
std::same_as<Quantity, dimensions::AngularVelocity> || std::same_as<Quantity, dimensions::AngularAcceleration> || std::same_as<Quantity, dimensions::AngularMomentum> ||
std::same_as<Quantity, dimensions::Force> || std::same_as<Quantity, dimensions::PressureLaplacian> ||
std::same_as<Quantity, dimensions::Velocity> || std::same_as<Quantity, dimensions::MassDensityRate> || std::same_as<Quantity, dimensions::ForceDensity>;
/** @brief Select a typed scale; temperature and other unrelated quantities require an explicit prescription. */
template <StellarScaleQuantity Quantity>
[[nodiscard]] dimensions::QuantityValue<Quantity> characteristic_scale(const StellarReferenceScales &scales, Quantity)
{
if constexpr (std::same_as<Quantity, dimensions::Dimensionless>)
{
return dimensions::QuantityValue<Quantity>{1.0};
}
else if constexpr (std::same_as<Quantity, dimensions::Mass>)
{
return scales.mass;
}
else if constexpr (std::same_as<Quantity, dimensions::Length>)
{
return scales.radius;
}
else if constexpr (std::same_as<Quantity, dimensions::MassDensity>)
{
return scales.density;
}
else if constexpr (std::same_as<Quantity, dimensions::Acceleration>)
{
return scales.acceleration;
}
else if constexpr (std::same_as<Quantity, dimensions::InverseTimeSquared> || std::same_as<Quantity, dimensions::AngularAcceleration>)
{
return dimensions::QuantityValue<Quantity>{scales.inverse_time_squared.value()};
}
else if constexpr (std::same_as<Quantity, dimensions::SpecificEnergy> || std::same_as<Quantity, dimensions::SpecificEnthalpy> ||
std::same_as<Quantity, dimensions::BarotropicPressurePotential> || std::same_as<Quantity, dimensions::GravitationalPotential>)
{
return dimensions::QuantityValue<Quantity>{scales.specific_energy.value()};
}
else if constexpr (std::same_as<Quantity, dimensions::Pressure>)
{
return scales.pressure;
}
else if constexpr (std::same_as<Quantity, dimensions::Velocity>)
{
return dimensions::QuantityValue<Quantity>{std::sqrt(scales.specific_energy.value())};
}
else if constexpr (std::same_as<Quantity, dimensions::MassDensityRate>)
{
return dimensions::QuantityValue<Quantity>{scales.density.value() * scales.angular_velocity.value()};
}
else if constexpr (std::same_as<Quantity, dimensions::ForceDensity>)
{
return dimensions::QuantityValue<Quantity>{scales.density.value() * scales.acceleration.value()};
}
else if constexpr (std::same_as<Quantity, dimensions::PressureLaplacian>)
{
return dimensions::QuantityValue<Quantity>{scales.pressure.value() / (scales.radius.value() * scales.radius.value())};
}
else if constexpr (std::same_as<Quantity, dimensions::AngularVelocity>)
{
return scales.angular_velocity;
}
else if constexpr (std::same_as<Quantity, dimensions::AngularMomentum>)
{
return scales.angular_momentum;
}
else
{
return scales.force;
}
}
}