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

View File

@@ -0,0 +1,89 @@
#include "serif/solver/normalization/diagonal.hpp"
#include <functional>
#include <stdexcept>
#include <utility>
#include "serif/solver/normalization/detail/validation.hpp"
#include "serif/utils/error/errors.hpp"
namespace serif::solver::normalization
{
namespace {
/** @brief Measure a physical vector in its prepared diagonal metric. */
[[nodiscard]] double normalization_norm_squared(std::span<const double> factors, std::span<const double> physical) {
detail::require_normalization_size(physical.size(), factors.size());
double norm_squared = 0.0;
for (std::size_t index = 0; index < physical.size(); ++index)
{
const double normalized = factors[index] * physical[index];
norm_squared += normalized * normalized;
}
return norm_squared;
}
}
void detail::require_normalization_size(std::size_t received, std::size_t expected) {
using namespace utils::errors;
serif_ensure<SERiFErrorCode::NORMALIZATION_VECTOR_SIZE_MISMATCH, std::invalid_argument>(received == expected, "Normalization received a vector of length {} but expected {}. Supply a vector in the prepared coordinate layout.", received, expected);
}
void detail::apply_normalization(const std::span<const double> factors, const std::span<const double> input, std::span<double> output, const bool inverse)
{
using namespace utils::errors;
require_normalization_size(input.size(), factors.size());
require_normalization_size(output.size(), factors.size());
if (input.data() != output.data() && !input.empty())
{
const std::less<const double*> before;
const bool input_ends_before_output = !before(output.data(), input.data() + input.size());
const bool output_ends_before_input = !before(input.data(), output.data() + output.size());
serif_ensure<SERiFErrorCode::NORMALIZATION_BUFFER_OVERLAP, std::invalid_argument>(input_ends_before_output || output_ends_before_input, "Normalization input and output overlap partially. Use the same span for an in-place conversion or provide disjoint storage.");
}
for (std::size_t index = 0; index < input.size(); ++index)
{
output[index] = inverse ? input[index] / factors[index] : factors[index] * input[index];
}
}
DiagonalNormalization::DiagonalNormalization(std::vector<double> state, std::vector<double> residual) : m_state_factors(std::move(state)), m_residual_factors(std::move(residual)) {}
void DiagonalNormalization::normalize_state(const std::span<const double> physical, const std::span<double> normalized) const {
detail::apply_normalization(m_state_factors, physical, normalized, false);
}
void DiagonalNormalization::denormalize_state(const std::span<const double> normalized, const std::span<double> physical) const {
detail::apply_normalization(m_state_factors, normalized, physical, true);
}
void DiagonalNormalization::normalize_residual(const std::span<const double> physical, const std::span<double> normalized) const {
detail::apply_normalization(m_residual_factors, physical, normalized, false);
}
void DiagonalNormalization::denormalize_residual(const std::span<const double> normalized, const std::span<double> physical) const {
detail::apply_normalization(m_residual_factors, normalized, physical, true);
}
double DiagonalNormalization::state_norm_squared(const std::span<const double> physical) const {
return normalization_norm_squared(m_state_factors, physical);
}
double DiagonalNormalization::residual_norm_squared(const std::span<const double> physical) const {
return normalization_norm_squared(m_residual_factors, physical);
}
std::expected<DiagonalNormalization, std::string> make_diagonal_normalization(std::vector<double> state, std::vector<double> residual) {
for (const std::span<const double> factors : {std::span<const double>{state}, std::span<const double>{residual}})
{
const auto valid = detail::validate_factors(factors);
if (!valid)
{
return std::unexpected(valid.error());
}
}
return DiagonalNormalization{std::move(state), std::move(residual)};
}
}

View File

@@ -0,0 +1,46 @@
#include "serif/solver/normalization/dimensionless.hpp"
#include <cmath>
#include <initializer_list>
#include "serif/utils/error/errors.hpp"
namespace serif::solver::normalization::detail {
std::expected<double, std::string> dimensionless_riesz_scale(
discretization::blocks::RepresentedValueKind kind, ReferenceGeometry geometry, double physical_scale, double reference_radius) {
using namespace utils::errors;
using Kind = discretization::blocks::RepresentedValueKind;
if (!std::isfinite(physical_scale) || physical_scale <= 0.0 || !std::isfinite(reference_radius) || reference_radius <= 0.0) {
return std::unexpected(format_error_message<SERiFErrorCode::NORMALIZATION_ERROR>(
"Dimensionless normalization needs finite positive physical and radius scales (received {} and {}). Choose a valid reference star.", physical_scale, reference_radius));
}
if (kind != Kind::value && kind != Kind::residual) {
return std::unexpected(format_error_message<SERiFErrorCode::INVALID_ARGUMENT>(
"Dimensionless normalization requires a value or residual coordinate kind. Supply a supported RepresentedValueKind."));
}
long double scale = physical_scale;
switch (geometry) {
case ReferenceGeometry::volume:
// Raw volume Gram matrices retain the physical basis, including the RT Piola transform.
scale *= static_cast<long double>(reference_radius) * std::sqrt(static_cast<long double>(reference_radius));
break;
case ReferenceGeometry::surface:
// Shape values are lengths; their dual coordinates are integrated forces, not force per area.
scale *= kind == Kind::value ? static_cast<long double>(reference_radius) : 1.0L / reference_radius;
break;
case ReferenceGeometry::algebraic:
break;
default:
return std::unexpected(format_error_message<SERiFErrorCode::INVALID_ARGUMENT>(
"Dimensionless normalization received an unknown reference geometry. Select volume, surface, or algebraic coordinates."));
}
const double result = static_cast<double>(scale);
if (!std::isfinite(result) || result <= 0.0) {
return std::unexpected(format_error_message<SERiFErrorCode::NORMALIZATION_ERROR>(
"The dimensionless normalization scale is not representable as a finite positive double. Change the reference units."));
}
return result;
}
}

View File

@@ -0,0 +1,52 @@
#include "serif/solver/normalization/detail/packing.hpp"
#include <cstddef>
#include <utility>
#include <vector>
#include "serif/utils/error/errors.hpp"
namespace serif::solver::normalization::detail
{
namespace
{
/** @brief Preserve the declared block order while checking the total before allocating packed storage. */
[[nodiscard]] std::expected<std::vector<double>, std::string> pack_factors(std::span<const std::span<const double>> coordinates)
{
using namespace utils::errors;
std::size_t count = 0;
for (const std::span<const double> factors : coordinates)
{
if (factors.size() > std::vector<double>{}.max_size() - count)
{
return std::unexpected(format_error_message<SERiFErrorCode::INVALID_DOF_COUNT>("The packed normalization layout exceeds the supported vector size. Reduce its coordinate counts."));
}
count += factors.size();
}
std::vector<double> packed;
packed.reserve(count);
for (const std::span<const double> factors : coordinates)
{
packed.insert(packed.end(), factors.begin(), factors.end());
}
return packed;
}
}
std::expected<DiagonalNormalization, std::string> pack_normalization(std::span<const std::span<const double>> states, std::span<const std::span<const double>> residuals)
{
auto state_factors = pack_factors(states);
if (!state_factors)
{
return std::unexpected(state_factors.error());
}
auto residual_factors = pack_factors(residuals);
if (!residual_factors)
{
return std::unexpected(residual_factors.error());
}
return make_diagonal_normalization(std::move(*state_factors), std::move(*residual_factors));
}
}

View File

@@ -0,0 +1,135 @@
#include "serif/solver/normalization/point_rows.hpp"
#include <cmath>
#include "serif/solver/normalization/diagonal.hpp"
#include "serif/utils/error/errors.hpp"
namespace serif::solver::normalization
{
namespace
{
using Kind = discretization::blocks::RepresentedValueKind;
/** @brief Validate units independently of whether any coordinate uses their reciprocal. */
[[nodiscard]] std::expected<void, std::string> validate_scale(double physical_scale)
{
using namespace utils::errors;
if (!std::isfinite(physical_scale) || physical_scale <= 0.0)
{
return std::unexpected(format_error_message<SERiFErrorCode::NORMALIZATION_ERROR>("A physical normalization scale must be finite and positive (received {}). Choose a valid reference scale.", physical_scale));
}
return {};
}
/** @brief Validate coordinate-wide settings once before scalar or bulk Riesz preparation. */
[[nodiscard]] std::expected<void, std::string> validate_riesz_parameters(Kind kind, double physical_scale)
{
using namespace utils::errors;
const auto valid_scale = validate_scale(physical_scale);
if (!valid_scale)
{
return valid_scale;
}
if (kind != Kind::value && kind != Kind::residual)
{
return std::unexpected(format_error_message<SERiFErrorCode::INVALID_ARGUMENT>("Riesz factors require a value or residual coordinate kind. Supply a supported RepresentedValueKind."));
}
return {};
}
/** @brief Compute one metric-dependent factor after the coordinate's role and physical scale are validated. */
[[nodiscard]] std::expected<double, std::string> riesz_factor(bool dual, double metric, double physical_scale, std::size_t coordinate)
{
using namespace utils::errors;
if (!std::isfinite(metric) || metric <= 0.0)
{
return std::unexpected(format_error_message<SERiFErrorCode::NORMALIZATION_ERROR>("Normalization metric entry {} must be finite and positive (received {}). Use a valid reference Gram diagonal.", coordinate, metric));
}
const double root_metric = std::sqrt(metric);
// Inverting the root first avoids overflow of scale * root_metric when its reciprocal is representable.
const double factor = dual ? (1.0 / root_metric) / physical_scale : root_metric / physical_scale;
if (!std::isfinite(factor) || factor <= 0.0)
{
return std::unexpected(format_error_message<SERiFErrorCode::NORMALIZATION_ERROR>("Normalization factor {} is not representable as a finite positive double for metric {} and scale {}. Change the reference units.", coordinate, metric, physical_scale));
}
return factor;
}
}
std::expected<double, std::string> make_riesz_factor(Kind kind, double metric, double physical_scale)
{
const auto valid = validate_riesz_parameters(kind, physical_scale);
if (!valid)
{
return std::unexpected(valid.error());
}
return riesz_factor(kind == Kind::residual, metric, physical_scale, 0);
}
std::expected<std::vector<double>, std::string> make_riesz_factors(Kind kind, std::span<const double> metric, double physical_scale)
{
const auto valid = validate_riesz_parameters(kind, physical_scale);
if (!valid)
{
return std::unexpected(valid.error());
}
const bool dual = kind == Kind::residual;
std::vector<double> factors(metric.size());
for (std::size_t index = 0; index < metric.size(); ++index)
{
const auto factor = riesz_factor(dual, metric[index], physical_scale, index);
if (!factor)
{
return std::unexpected(factor.error());
}
factors[index] = *factor;
}
return factors;
}
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)
{
using namespace utils::errors;
for (const double scale : {volume_scale, point_scale})
{
const auto valid = validate_scale(scale);
if (!valid)
{
return std::unexpected(valid.error());
}
}
std::vector<bool> replaced(volume_metric.size(), false);
for (const int row : selected_rows)
{
if (row < 0 || static_cast<std::size_t>(row) >= volume_metric.size())
{
return std::unexpected(format_error_message<SERiFErrorCode::OUT_OF_RANGE_ERROR>("Replacement normalization row {} lies outside its residual coordinate of size {}. Use the physical equation's reduced row selection.", row, volume_metric.size()));
}
if (replaced[row])
{
return std::unexpected(format_error_message<SERiFErrorCode::INVALID_ARGUMENT>("Residual row {} was selected more than once for point normalization. Supply disjoint replacement rows.", row));
}
replaced[row] = true;
}
std::vector<double> factors(volume_metric.size());
for (std::size_t row = 0; row < factors.size(); ++row)
{
const double metric = replaced[row] ? 1.0 : volume_metric[row];
const double scale = replaced[row] ? point_scale : volume_scale;
const auto factor = riesz_factor(true, metric, scale, row);
if (!factor)
{
return std::unexpected(factor.error());
}
factors[row] = *factor;
}
return factors;
}
}

View File

@@ -0,0 +1,62 @@
#include "serif/solver/normalization/scales.hpp"
#include <cmath>
#include "serif/utils/error/errors.hpp"
namespace serif::solver::normalization
{
/** @brief Derive gravitational stellar scales from a finite, positive reference mass, radius, and G. */
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)
{
using namespace utils::errors;
for (const double value : {mass.value(), radius.value(), gravitational_constant.value()})
{
if (!std::isfinite(value) || value <= 0.0)
{
return std::unexpected(format_error_message<SERiFErrorCode::NORMALIZATION_ERROR>("Stellar normalization requires finite, positive reference mass, radius, and gravitational constant. Correct the reference values."));
}
}
const double reference_mass = mass.value();
const double reference_radius = radius.value();
const double gravity = gravitational_constant.value();
const double radius_squared = reference_radius * reference_radius;
const double radius_cubed = radius_squared * reference_radius;
const double density = reference_mass / radius_cubed;
const double acceleration = gravity * reference_mass / radius_squared;
const double inverse_time_squared = gravity * reference_mass / radius_cubed;
const double specific_energy = gravity * reference_mass / reference_radius;
const double pressure = density * specific_energy;
const double angular_velocity = std::sqrt(inverse_time_squared);
const double angular_momentum = reference_mass * std::sqrt(gravity * reference_mass * reference_radius);
const double force = reference_mass * acceleration;
for (const double value : {density, acceleration, inverse_time_squared, specific_energy, pressure, angular_velocity, angular_momentum, force})
{
if (!std::isfinite(value) || value <= 0.0)
{
return std::unexpected(format_error_message<SERiFErrorCode::NORMALIZATION_ERROR>("A characteristic stellar normalization scale is not finite and positive; choose reference units in which every derived scale is representable."));
}
}
return StellarReferenceScales{
.mass = mass,
.radius = radius,
.gravitational_constant = gravitational_constant,
.density = dimensions::QuantityValue<dimensions::MassDensity>{density},
.acceleration = dimensions::QuantityValue<dimensions::Acceleration>{acceleration},
.inverse_time_squared = dimensions::QuantityValue<dimensions::InverseTimeSquared>{inverse_time_squared},
.specific_energy = dimensions::QuantityValue<dimensions::SpecificEnergy>{specific_energy},
.pressure = dimensions::QuantityValue<dimensions::Pressure>{pressure},
.angular_velocity = dimensions::QuantityValue<dimensions::AngularVelocity>{angular_velocity},
.angular_momentum = dimensions::QuantityValue<dimensions::AngularMomentum>{angular_momentum},
.force = dimensions::QuantityValue<dimensions::Force>{force}
};
}
}

View File

@@ -0,0 +1,49 @@
#include "serif/solver/normalization/detail/validation.hpp"
#include <cmath>
#include <vector>
#include "serif/utils/error/errors.hpp"
namespace serif::solver::normalization::detail
{
std::expected<void, std::string> validate_factors(std::span<const double> factors)
{
using namespace utils::errors;
for (std::size_t index = 0; index < factors.size(); ++index)
{
if (!std::isfinite(factors[index]) || factors[index] <= 0.0)
{
return std::unexpected(format_error_message<SERiFErrorCode::NORMALIZATION_ERROR>("Normalization factor {} is not finite and positive (received {}). Supply finite positive factors for the declared coordinate.", index, factors[index]));
}
}
return {};
}
std::expected<std::size_t, std::string> validate_coordinate_size(std::size_t size, bool global_scalar)
{
using namespace utils::errors;
if (size > std::vector<double>{}.max_size())
{
return std::unexpected(format_error_message<SERiFErrorCode::NORMALIZATION_VECTOR_SIZE_MISMATCH>("A normalization coordinate requests {} entries, exceeding supported vector storage. Reduce its state or residual layout count.", size));
}
if (global_scalar && size != 1)
{
return std::unexpected(format_error_message<SERiFErrorCode::NORMALIZATION_VECTOR_SIZE_MISMATCH>("A global scalar normalization coordinate requires one entry, but its layout declares {}. Correct its state or residual layout.", size));
}
return size;
}
std::expected<void, std::string> check_coordinate_size(std::size_t expected, std::size_t received)
{
using namespace utils::errors;
if (expected != received)
{
return std::unexpected(format_error_message<SERiFErrorCode::NORMALIZATION_VECTOR_SIZE_MISMATCH>("Normalization received {} entries for a coordinate with {} entries. Use the metric and ordering of its reference representation.", received, expected));
}
return {};
}
}