Files
MeanField_SERiF/tests/sandbox/normalization_sandbox.cpp
Emily Boudreaux cb20faeb3d test(tests): added test suite
The test suite is extensive. Note that LLMs were used to aid in the development of the test suite. All LLM generated code was manually reviewed.
2026-11-09 13:53:17 -05:00

146 lines
8.5 KiB
C++

#include <array>
#include <cmath>
#include <cstdio>
#include <print>
#include <tuple>
#include <utility>
#include <vector>
#include "serif/equations/mass_normalization.hpp"
#include "serif/equations/primal_gravity.hpp"
#include "serif/solver/normalization/normalization.hpp"
#include "serif/utils/dependencies/computation_cache.hpp"
namespace
{
namespace normalization = serif::solver::normalization;
namespace dependencies = serif::utils::dependencies;
namespace dimensions = serif::dimensions;
/** @brief Convert illustrative physical coefficients and assembled residuals into one solve's fixed coordinates. */
void show_coordinate_conversions(const normalization::DiagonalNormalization& coordinates, const std::array<double, 3>& physical_state, const std::array<double, 3>& physical_residual)
{
// These orders follow the layout below: [potential, surface potential level] and [Poisson balance, mass constraint].
std::array<double, 3> normalized_state{};
std::array<double, 3> normalized_residual{};
coordinates.normalize_state(physical_state, normalized_state);
coordinates.normalize_residual(physical_residual, normalized_residual);
std::println("Physical potential coefficients: [{:.6g}, {:.6g}]; surface potential level: {:.6g}", physical_state[0], physical_state[1], physical_state[2]);
std::println("Normalized potential coefficients: [{:.6g}, {:.6g}]; surface potential level: {:.6g}", normalized_state[0], normalized_state[1], normalized_state[2]);
std::println("Physical assembled Poisson residual: [{:.6g}, {:.6g}]; mass residual: {:.6g}", physical_residual[0], physical_residual[1], physical_residual[2]);
std::println("Normalized Poisson residual: [{:.6g}, {:.6g}]; mass residual: {:.6g}", normalized_residual[0], normalized_residual[1], normalized_residual[2]);
std::println("Residual norm in these coordinates: {:.6g}", std::sqrt(coordinates.residual_norm_squared(physical_residual)));
// Physical equations still consume physical coefficients. Convert a solver's coordinates back before evaluation.
std::array<double, 3> recovered_state{};
coordinates.denormalize_state(normalized_state, recovered_state);
std::println("Recovered physical potential coefficients: [{:.6g}, {:.6g}]; surface potential level: {:.6g}", recovered_state[0], recovered_state[1], recovered_state[2]);
}
}
/**
* @brief Prepare, apply, reuse, and explicitly replace the normalization for a small selection of stellar coordinates.
*
* Potential and the surface potential level use GM/R, Poisson balance uses GM/R^3, and the mass constraint uses M.
* PhysicalValueRule reads the unknown's Quantity; PhysicalResidualRule reads the equation's ResidualQuantity.
* Those quantities need not agree, even when an equation closes a particular unknown.
*
* The illustrative metric contains two diagonal entries m_i = integral psi_i^2 dV_ref in the field's coefficient order.
* A configured stellar discretization builds reference metrics in its computational graph automatically.
* complete_sandbox demonstrates that path; this small example supplies a diagonal explicitly.
* State factors are sqrt(m_i)/state_scale; assembled weak-form residual factors are 1/(sqrt(m_i)*residual_scale).
* Algebraic coordinates use a unit metric. This is a diagonal metric, not the full mass-matrix norm.
*
* The example starts with supplied coefficients and assembled residuals; it does not assemble a complete stellar model.
* Reference units must be consistent with those physical quantities and the reference geometry.
*/
int main()
{
using Potential = serif::discretization::blocks::gravity::potential;
using SurfaceLevel = serif::discretization::blocks::barotropic_constant::mass_normalization;
using PoissonBalance = serif::equations::PrimalPoissonBalance;
using MassConstraint = serif::equations::MassNormalization;
using PotentialMetric = normalization::ReferenceVolumeMetric<Potential>;
// The plan chooses physical scaling. The layout independently chooses the order and sizes of packed blocks.
using Plan = normalization::NormalizationPlan<
dependencies::Bindings<normalization::PhysicalValueRule<Potential>, normalization::PhysicalValueRule<SurfaceLevel>>,
dependencies::Bindings<normalization::PhysicalResidualRule<PoissonBalance>, normalization::PhysicalResidualRule<MassConstraint>>>;
const normalization::CoordinateLayout<dependencies::KeyList<Potential, SurfaceLevel>, dependencies::KeyList<PoissonBalance, MassConstraint>> layout{
.state_sizes = {2, 1},
.residual_sizes = {2, 1}
};
const dimensions::QuantityValue<dimensions::Length> reference_radius{2.0};
const dimensions::QuantityValue<dimensions::GravitationalConstant> gravitational_constant{1.0};
auto reference = normalization::prepare_stellar_reference_scales(dimensions::QuantityValue<dimensions::Mass>{8.0}, reference_radius, gravitational_constant);
if (!reference)
{
std::println(stderr, "Cannot prepare reference scales: {}", reference.error());
return 1;
}
normalization::StellarReferenceScales reference_scales = *reference;
const std::vector<double> reference_metric{4.0, 9.0};
// Borrowed inputs must outlive the cache. Trial coefficients are absent: changing a candidate cannot change its units.
const auto inputs = dependencies::make_dependency_context(
dependencies::borrow<normalization::keys::Layout>(layout),
dependencies::borrow<normalization::keys::Scales>(reference_scales),
dependencies::borrow<PotentialMetric>(reference_metric));
// Factories only declare work. Both coordinate roles share this ordinary dependency cache.
auto computations = normalization::make_normalization_computations<Plan>(layout);
auto cache = std::apply([&](auto... nodes)
{
return dependencies::make_computation_cache(inputs, std::move(nodes)...);
}, std::move(computations));
const auto prepared = cache.try_prepare(inputs);
if (!prepared)
{
std::println(stderr, "Cannot prepare reference normalization: {}", prepared.error().error);
return 1;
}
const normalization::StellarReferenceScales& scales = reference_scales;
std::println("Reference mass: {:.6g}; radius: {:.6g}; G: {:.6g}", scales.mass.value(), scales.radius.value(), scales.gravitational_constant.value());
std::println("Potential and surface-potential scale: {:.6g}; Poisson scale: {:.6g}; mass scale: {:.6g}", scales.specific_energy.value(), scales.inverse_time_squared.value(), scales.mass.value());
// This owning copy fixes the coordinates for a solve, even if the cache later prepares another reference.
const normalization::DiagonalNormalization solve_coordinates = cache.get<normalization::keys::Normalization>();
const std::array physical_state{-8.0, -12.0, 6.0};
const std::array physical_residual{0.2, -0.3, 0.4};
show_coordinate_conversions(solve_coordinates, physical_state, physical_residual);
const auto unchanged = cache.try_prepare(inputs);
if (!unchanged)
{
std::println(stderr, "Cannot reuse reference normalization: {}", unchanged.error().error);
return 1;
}
std::println("Unchanged reference: {} computations repeated.", unchanged->computed_count());
// After this solve, explicitly choose a new reference mass for the next solve. The accepted copy remains valid.
reference = normalization::prepare_stellar_reference_scales(dimensions::QuantityValue<dimensions::Mass>{16.0}, reference_radius, gravitational_constant);
if (!reference)
{
std::println(stderr, "Cannot prepare the next reference scales: {}", reference.error());
return 1;
}
reference_scales = *reference;
const auto updated = cache.try_prepare(inputs);
if (!updated)
{
std::println(stderr, "Cannot prepare the next reference normalization: {}", updated.error().error);
return 1;
}
const normalization::DiagonalNormalization next_solve_coordinates = cache.get<normalization::keys::Normalization>();
std::array<double, 3> old_reference_state{};
std::array<double, 3> new_reference_state{};
solve_coordinates.normalize_state(physical_state, old_reference_state);
next_solve_coordinates.normalize_state(physical_state, new_reference_state);
std::println("After doubling reference mass, surface potential level in original coordinates: {:.6g}; next solve's coordinates: {:.6g}", old_reference_state[2], new_reference_state[2]);
return 0;
}