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.
This commit is contained in:
809
tests/model/moving_equilibrium.cpp
Normal file
809
tests/model/moving_equilibrium.cpp
Normal file
@@ -0,0 +1,809 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <numbers>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "mfem/linalg/solvers.hpp"
|
||||
#include "mfem/general/communication.hpp"
|
||||
|
||||
#include "serif/equations/angular_momentum.hpp"
|
||||
#include "serif/equations/barotropic_density.hpp"
|
||||
#include "serif/equations/primal_gravity.hpp"
|
||||
#include "serif/equations/surface_potential.hpp"
|
||||
#include "serif/equations/mass_normalization.hpp"
|
||||
#include "serif/backend/mfem/model/equilibrium.hpp"
|
||||
#include "serif/model/eos_reference.hpp"
|
||||
#include "serif/solver/preconditioner/default_preconditioners.hpp"
|
||||
#include "serif/tests/deformation/mesh.hpp"
|
||||
#include "serif/tests/equations/gravity_fixture.hpp"
|
||||
|
||||
// MeanField's prepared_stellar_equilibrium.cpp checks the complete moving root
|
||||
// against its children and centered differences. Keep both checks: child parity
|
||||
// verifies composition, while finite differences verify the assembled derivative.
|
||||
namespace {
|
||||
namespace model = serif::model;
|
||||
namespace backend = model::backend::mfem;
|
||||
namespace execution = serif::operators::backend::mfem;
|
||||
namespace dependencies = serif::utils::dependencies;
|
||||
namespace equations = serif::equations;
|
||||
namespace deformation = serif::geometry::deformation;
|
||||
namespace surface = serif::geometry::surface;
|
||||
namespace blocks = serif::discretization::blocks;
|
||||
namespace tests = serif::tests::equations;
|
||||
using Polytrope = serif::eos::models::Polytrope;
|
||||
using Density = blocks::density::mass;
|
||||
using Potential = blocks::gravity::potential;
|
||||
using Constant = blocks::barotropic_constant::mass_normalization;
|
||||
using Omega = blocks::rotation::angular_velocity;
|
||||
using Shape = blocks::surface_deformation::coefficients;
|
||||
using ShapeRow = blocks::surface_deformation::shape_equilibrium;
|
||||
using Displacement = blocks::displacement::vector;
|
||||
using Kind = blocks::RepresentedValueKind;
|
||||
using Vector = mfem::Vector;
|
||||
using InteriorRows = dependencies::KeyList<Density, Potential, Constant, Omega>;
|
||||
|
||||
/** @brief Own paired stellar/vacuum meshes and all resources borrowed by the public model binding. */
|
||||
struct Fixture {
|
||||
stroid::StroidMesh generated{serif::tests::deformation::make_mesh(1)};
|
||||
tests::gravity::GravityFixture fields;
|
||||
mfem::FiniteElementSpace reference_displacement{&fields.mesh, &fields.enthalpy_collection, 3, mfem::Ordering::byNODES};
|
||||
mfem::FiniteElementSpace compactification_space{&fields.mesh, generated.exterior_coordinate->space->FEColl()};
|
||||
mfem::GridFunction compactification{&compactification_space};
|
||||
|
||||
/** @brief Preserve the generator's exterior coordinate on the identically numbered mesh copy. */
|
||||
explicit Fixture(const mfem::Ordering::Type ordering) : fields(mfem::Mesh(*generated.mesh), ordering) {
|
||||
REQUIRE(compactification.Size() == generated.exterior_coordinate->values->Size());
|
||||
compactification = *generated.exterior_coordinate->values;
|
||||
}
|
||||
};
|
||||
|
||||
/** @brief Exercise all stellar equations at a valid, deliberately unconverged rotating state. */
|
||||
[[nodiscard]] auto physical_model(const double index = 1.5) {
|
||||
const auto definition = [](const auto& physics) {
|
||||
const auto eos = model::borrow_eos(physics.equation_of_state());
|
||||
const auto& rotation = physics.template specification<model::FixedAngularMomentum>();
|
||||
const auto& axis = rotation.axis();
|
||||
const auto& center = rotation.center();
|
||||
return model::EquilibriumEquations{
|
||||
.interior_equations = equations::EquationRegistry{
|
||||
equations::BarotropicDensityBalance{eos, serif::dimensions::PressureValue{0.025}, axis, center}, equations::PrimalPoissonBalance{2.3},
|
||||
equations::MassNormalization{physics.template specification<model::FixedTotalMass>().target().value()},
|
||||
equations::AngularMomentumConstraint{rotation.target().value(), axis, center}
|
||||
},
|
||||
.shape_force = equations::SurfacePotentialBalance{axis, center},
|
||||
.surface_condition = surface::Isobaric{serif::dimensions::PressureValue{0.025}}
|
||||
};
|
||||
};
|
||||
return model::make_stellar_model(Polytrope{index, 0.25}, definition,
|
||||
model::FixedTotalMass{serif::dimensions::MassValue{1.1}},
|
||||
model::FixedAngularMomentum{{.angular_momentum = serif::dimensions::AngularMomentumValue{0.14},
|
||||
.axis = {0.2, -0.3, 1.0}, .center = {0.03, -0.02, 0.01}}});
|
||||
}
|
||||
|
||||
/** @brief Use the production radial lift and vacuum extension with nine independent surface harmonics. */
|
||||
[[nodiscard]] auto discretize(const std::shared_ptr<Fixture>& fixture) {
|
||||
const auto& fields = fixture->fields;
|
||||
const auto bindings = dependencies::make_dependency_context(
|
||||
dependencies::borrow<Density>(fields.density_binding), dependencies::borrow<Potential>(fields.potential_binding),
|
||||
dependencies::borrow<Constant>(fields.constant_binding), dependencies::borrow<Omega>(fields.angular_velocity_binding),
|
||||
dependencies::borrow<Displacement>(fields.displacement_binding));
|
||||
const auto reference = deformation::backend::mfem::deformation_discretization(
|
||||
fields.enthalpy_space, fixture->reference_displacement, *fixture->generated.reference_mesh);
|
||||
const deformation::DomainDeformation<deformation::SphericalHarmonicSurface> geometry{
|
||||
.surface = {.maximum_degree = 2}, .stellar_interior = {}, .vacuum = {}
|
||||
};
|
||||
return backend::with_deformation(backend::make_stellar_discretization(fixture, bindings, fixture->compactification, fields.mapper), geometry, reference);
|
||||
}
|
||||
|
||||
/** @brief Populate all physical fields, scalar constraints and surface modes in the moving-star fixture. */
|
||||
template <typename Layout, typename ShapeCoefficients>
|
||||
[[nodiscard]] model::StellarState<Layout> initial_state(const Layout& layout, tests::gravity::GravityFixture& fields, const ShapeCoefficients& shape) {
|
||||
model::StellarState state(layout);
|
||||
const auto initialize = [&]<typename Term>(const Vector& values) {
|
||||
const auto destination = state.template get<Term>();
|
||||
REQUIRE(destination.size() == static_cast<std::size_t>(values.Size()));
|
||||
std::copy_n(values.GetData(), values.Size(), destination.begin());
|
||||
};
|
||||
initialize.template operator()<Density>(fields.density());
|
||||
initialize.template operator()<Potential>(fields.potential());
|
||||
state.template get<Constant>()[0] = 0.72;
|
||||
state.template get<Omega>()[0] = 0.31;
|
||||
std::ranges::copy(shape, state.template get<Shape>().begin());
|
||||
return state;
|
||||
}
|
||||
|
||||
/** @brief Make an independent physical candidate without sharing any mutable field storage. */
|
||||
template <typename Layout>
|
||||
[[nodiscard]] model::StellarState<Layout> perturbed_state(const model::StellarState<Layout>& original, const double step, const Vector& direction) {
|
||||
REQUIRE(original.values().size() == static_cast<std::size_t>(direction.Size()));
|
||||
model::StellarState result(original);
|
||||
for (int index = 0; index < direction.Size(); ++index) {
|
||||
result.values()[static_cast<std::size_t>(index)] += step * direction[index];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** @brief Copy a frozen residual before evaluating another candidate. */
|
||||
template <typename System>
|
||||
[[nodiscard]] Vector residual(const System& system) {
|
||||
Vector result(static_cast<int>(system.residual().size()));
|
||||
std::copy(system.residual().begin(), system.residual().end(), result.GetData());
|
||||
return result;
|
||||
}
|
||||
|
||||
/** @brief Check each equation separately so large gravity rows cannot hide missing scalar or shape rows. */
|
||||
template <typename Layout>
|
||||
void check_rows(const Vector& actual, const Vector& expected, const Layout& layout, double relative_tolerance, double absolute_tolerance) {
|
||||
REQUIRE(actual.Size() == expected.Size());
|
||||
int offset = 0;
|
||||
for (std::size_t row = 0; row < layout.residual_sizes.size(); ++row) {
|
||||
CAPTURE(row);
|
||||
double error_squared = 0.0, reference_squared = 0.0;
|
||||
for (std::size_t entry = 0; entry < layout.residual_sizes[row]; ++entry) {
|
||||
const double difference = actual[offset] - expected[offset];
|
||||
error_squared += difference * difference;
|
||||
reference_squared += expected[offset] * expected[offset];
|
||||
++offset;
|
||||
}
|
||||
CHECK(std::sqrt(error_squared) <= absolute_tolerance + relative_tolerance * std::sqrt(reference_squared));
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Compare cached scalar functionals with the independently retained moving volume actions. */
|
||||
template <typename Row, typename Evaluator>
|
||||
void check_scalar_row(const Evaluator& evaluator, const backend::PhysicalSystem& physical) {
|
||||
const auto& layout = evaluator.layout();
|
||||
const auto& cache = evaluator.cache();
|
||||
const auto volume = cache.template share<typename execution::volume_keys::Scoped<Row>::Linearization>();
|
||||
const auto& shape = cache.template get<model::computations::ShapeDerivatives>();
|
||||
const auto reference = backend::volume_row_action(volume, layout, backend::ColumnContribution{
|
||||
static_cast<int>(layout.template offset<Kind::value, Shape>()), shape.tangent});
|
||||
const auto& cached = cache.template get<model::computations::ScalarResidualRow<Row>>();
|
||||
const Vector direction = tests::deterministic(reference.action()->Width(), 0.47);
|
||||
const Vector dual = tests::constant(1, 0.37);
|
||||
Vector expected(1), actual(1), expected_transpose, actual_transpose(direction.Size());
|
||||
reference.action()->Mult(direction, expected);
|
||||
cached.action()->Mult(direction, actual);
|
||||
CHECK(actual[0] == Catch::Approx(expected[0]).margin(2.0e-12));
|
||||
reference.action()->MultTranspose(dual, expected_transpose);
|
||||
cached.action()->MultTranspose(dual, actual_transpose);
|
||||
CHECK(tests::relative_error(actual_transpose, expected_transpose) < 2.0e-12);
|
||||
const auto check_column = [&]<typename Column>() {
|
||||
const int offset = static_cast<int>(layout.template offset<Kind::value, Column>());
|
||||
const int size = static_cast<int>(layout.template size<Kind::value, Column>());
|
||||
const auto selected = physical.block(static_cast<int>(layout.template offset<Kind::residual, Row>()), 1, offset, size);
|
||||
Vector input(size), selected_transpose;
|
||||
std::copy_n(direction.GetData() + offset, size, input.GetData());
|
||||
Vector complete(direction.Size());
|
||||
complete = 0.0;
|
||||
std::copy_n(input.GetData(), size, complete.GetData() + offset);
|
||||
cached.action()->Mult(complete, expected);
|
||||
selected.action()->Mult(input, actual);
|
||||
CHECK(actual[0] == Catch::Approx(expected[0]).margin(2.0e-12));
|
||||
selected.action()->MultTranspose(dual, selected_transpose);
|
||||
Vector expected_column(expected_transpose.GetData() + offset, size);
|
||||
CHECK(tests::relative_error(selected_transpose, expected_column) < 2.0e-12);
|
||||
};
|
||||
check_column.template operator()<Density>();
|
||||
check_column.template operator()<Shape>();
|
||||
check_column.template operator()<Omega>();
|
||||
}
|
||||
|
||||
/** @brief Place the volume and surface children, then independently enforce both physical traces. */
|
||||
template <typename Evaluator, typename Layout>
|
||||
void check_composition(const Evaluator& evaluator, const model::StellarState<Layout>& state,
|
||||
const backend::PhysicalSystem& physical, const serif::discretization::backend::mfem::DofSelection& boundary) {
|
||||
const auto& layout = evaluator.layout();
|
||||
const auto& cache = evaluator.cache();
|
||||
Vector expected(static_cast<int>(layout.template total_size<Kind::residual>()));
|
||||
expected = 0.0;
|
||||
[&]<typename... Rows>(dependencies::KeyList<Rows...>) {
|
||||
const auto place = [&]<typename Row>() {
|
||||
const auto& child = cache.template get<typename execution::volume_keys::Scoped<Row>::Linearization>();
|
||||
REQUIRE(static_cast<std::size_t>(child.residual.Size()) == layout.template size<Kind::residual, Row>());
|
||||
std::copy_n(child.residual.GetData(), child.residual.Size(), expected.GetData() + layout.template offset<Kind::residual, Row>());
|
||||
};
|
||||
(place.template operator()<Rows>(), ...);
|
||||
}(InteriorRows{});
|
||||
const auto& surface_child = cache.template get<model::computations::ShapeContribution>();
|
||||
REQUIRE(surface_child.residual.size() == 1);
|
||||
for (const auto& contribution : surface_child.residual) {
|
||||
std::copy(contribution.values.begin(), contribution.values.end(), expected.GetData() + contribution.row_offset);
|
||||
}
|
||||
const int shape_row = static_cast<int>(layout.template offset<Kind::residual, ShapeRow>());
|
||||
const int shape_size = static_cast<int>(layout.template size<Kind::residual, ShapeRow>());
|
||||
Vector shape_residual(expected.GetData() + shape_row, shape_size);
|
||||
REQUIRE(shape_residual.Norml2() > 1.0e-8);
|
||||
|
||||
// Compare each retained surface child coupling with the corresponding assembled block.
|
||||
for (const auto& contribution : surface_child.jacobian) {
|
||||
const auto selected = physical.block(contribution.row_offset, contribution.block.action()->Height(),
|
||||
contribution.column_offset, contribution.block.action()->Width());
|
||||
const Vector direction = tests::deterministic(contribution.block.action()->Width(), 0.47);
|
||||
const Vector dual = tests::deterministic(contribution.block.action()->Height(), 0.71);
|
||||
Vector actual(contribution.block.action()->Height()), reference(contribution.block.action()->Height());
|
||||
Vector actual_transpose(contribution.block.action()->Width()), reference_transpose(contribution.block.action()->Width());
|
||||
selected.action()->Mult(direction, actual);
|
||||
contribution.block.action()->Mult(direction, reference);
|
||||
CHECK(tests::relative_error(actual, reference) < 2.0e-13);
|
||||
selected.action()->MultTranspose(dual, actual_transpose);
|
||||
contribution.block.action()->MultTranspose(dual, reference_transpose);
|
||||
CHECK(tests::relative_error(actual_transpose, reference_transpose) < 2.0e-13);
|
||||
}
|
||||
|
||||
const auto density = state.template get<Density>();
|
||||
REQUIRE(boundary.size() > 0);
|
||||
REQUIRE(boundary.size() < static_cast<int>(density.size()));
|
||||
const double surface_density = serif::eos::evaluate<serif::dimensions::MassDensity>(
|
||||
evaluator.model().equation_of_state(), serif::dimensions::PressureValue{0.025}).value();
|
||||
const int density_row = static_cast<int>(layout.template offset<Kind::residual, Density>());
|
||||
for (const int row : boundary.reduced_dofs()) {
|
||||
expected[density_row + row] = density[row] - surface_density;
|
||||
}
|
||||
const auto infinity = serif::discretization::backend::mfem::select_boundary_dofs<
|
||||
serif::discretization::domain::InfinitySurfaceBoundary>(
|
||||
evaluator.binding().discretization->template binding<Potential>());
|
||||
REQUIRE(infinity.size() > 0);
|
||||
const int potential_row = static_cast<int>(layout.template offset<Kind::residual, Potential>());
|
||||
for (const int row : infinity.reduced_dofs()) {
|
||||
expected[potential_row + row] = state.template get<Potential>()[row];
|
||||
}
|
||||
check_rows(residual(physical), expected, layout, 2.0e-13, 2.0e-13);
|
||||
|
||||
// Each strong trace must reach its own scalar field alone, including in the transpose.
|
||||
const auto check_trace = [&]<typename Field>(const auto& selected) {
|
||||
Vector dual(expected.Size()), transpose;
|
||||
dual = 0.0;
|
||||
Vector expected_transpose(physical.jacobian().action()->Width());
|
||||
expected_transpose = 0.0;
|
||||
const int row_offset = static_cast<int>(layout.template offset<Kind::residual, Field>());
|
||||
const int column_offset = static_cast<int>(layout.template offset<Kind::value, Field>());
|
||||
for (const int row : selected.reduced_dofs()) {
|
||||
dual[row_offset + row] = 0.17 + 0.01 * (row % 7);
|
||||
expected_transpose[column_offset + row] = dual[row_offset + row];
|
||||
}
|
||||
physical.jacobian().action()->MultTranspose(dual, transpose);
|
||||
CHECK(tests::relative_error(transpose, expected_transpose) < 2.0e-13);
|
||||
};
|
||||
check_trace.template operator()<Density>(boundary);
|
||||
check_trace.template operator()<Potential>(infinity);
|
||||
}
|
||||
|
||||
/** @brief Compare every physical state column and a mixed perturbation through the public evaluation path. */
|
||||
template <typename Evaluator, typename Layout>
|
||||
void check_derivatives(Evaluator& evaluator, const model::StellarState<Layout>& state) {
|
||||
const auto& layout = evaluator.layout();
|
||||
const auto evaluation = evaluator.evaluate(state);
|
||||
REQUIRE(evaluation);
|
||||
const auto frozen = *evaluation;
|
||||
const Vector direction_seed = tests::deterministic(static_cast<int>(state.values().size()), 0.43);
|
||||
const Vector dual = tests::deterministic(frozen->jacobian().action()->Height(), 1.17);
|
||||
Vector transpose, block_transpose;
|
||||
frozen->jacobian().action()->MultTranspose(dual, transpose);
|
||||
const auto blocks = frozen->block(0, dual.Size(), 0, static_cast<int>(state.values().size()));
|
||||
blocks.action()->MultTranspose(dual, block_transpose);
|
||||
CHECK(tests::relative_error(transpose, block_transpose) < 2.0e-12);
|
||||
|
||||
// The final iteration moves all physical fields and the surface simultaneously.
|
||||
int offset = 0;
|
||||
for (std::size_t column = 0; column <= layout.state_sizes.size(); ++column) {
|
||||
CAPTURE(column);
|
||||
Vector direction(direction_seed);
|
||||
if (column < layout.state_sizes.size()) {
|
||||
direction = 0.0;
|
||||
std::copy_n(direction_seed.GetData() + offset, layout.state_sizes[column], direction.GetData() + offset);
|
||||
offset += static_cast<int>(layout.state_sizes[column]);
|
||||
}
|
||||
Vector action, block_action;
|
||||
frozen->jacobian().action()->Mult(direction, action);
|
||||
blocks.action()->Mult(direction, block_action);
|
||||
REQUIRE(action.Norml2() > 1.0e-10);
|
||||
check_rows(action, block_action, layout, 2.0e-12, 2.0e-12);
|
||||
CHECK((action * dual) == Catch::Approx(direction * transpose).epsilon(2.0e-12).margin(2.0e-12));
|
||||
|
||||
constexpr double step = 1.0e-5;
|
||||
const auto plus = evaluator.evaluate(perturbed_state(state, step, direction));
|
||||
REQUIRE(plus);
|
||||
Vector difference = residual(**plus);
|
||||
const auto minus = evaluator.evaluate(perturbed_state(state, -step, direction));
|
||||
REQUIRE(minus);
|
||||
difference -= residual(**minus);
|
||||
difference /= 2.0 * step;
|
||||
check_rows(action, difference, layout, 2.0e-6, 2.0e-9);
|
||||
|
||||
if (column == Layout::template index<Kind::value, Shape>()) {
|
||||
// Shape must affect each interior equation and the surface potential, not only its own row.
|
||||
int row_offset = 0;
|
||||
for (std::size_t row = 0; row < layout.residual_sizes.size(); ++row) {
|
||||
Vector row_action(action.GetData() + row_offset, static_cast<int>(layout.residual_sizes[row]));
|
||||
CHECK(row_action.Norml2() > 1.0e-10);
|
||||
row_offset += row_action.Size();
|
||||
}
|
||||
}
|
||||
}
|
||||
const auto restored = evaluator.evaluate(state);
|
||||
REQUIRE(restored);
|
||||
check_rows(residual(**restored), residual(*frozen), layout, 0.0, 0.0);
|
||||
}
|
||||
|
||||
/** @brief Check the assembled solver coordinates, including strong rows, scalar constraints and native surface modes. */
|
||||
template <typename Evaluator, typename Layout>
|
||||
void check_normalized_system(Evaluator& evaluator, const model::StellarState<Layout>& state,
|
||||
const serif::discretization::backend::mfem::DofSelection& boundary) {
|
||||
namespace normalization = serif::solver::normalization;
|
||||
namespace dimensions = serif::dimensions;
|
||||
const auto evaluation = evaluator.evaluate(state);
|
||||
REQUIRE(evaluation);
|
||||
const auto frozen = *evaluation;
|
||||
REQUIRE(frozen->physical() == evaluator.cache().template get<model::keys::PhysicalSystem>());
|
||||
const auto& layout = evaluator.layout();
|
||||
const auto& coordinates = frozen->coordinates();
|
||||
const auto& scales = frozen->reference_scales();
|
||||
CHECK(scales.mass.value() == Catch::Approx(1.1));
|
||||
CHECK(scales.radius.value() > 0.0);
|
||||
CHECK(scales.radius.value() != 1.0);
|
||||
REQUIRE(coordinates.state_size() == state.values().size());
|
||||
REQUIRE(coordinates.residual_size() == frozen->residual().size());
|
||||
CHECK(coordinates.state_factors().front() != 1.0);
|
||||
Vector expected_residual(static_cast<int>(frozen->residual().size()));
|
||||
coordinates.normalize_residual(frozen->physical()->residual(), {expected_residual.GetData(), frozen->residual().size()});
|
||||
check_rows(residual(*frozen), expected_residual, layout, 0.0, 0.0);
|
||||
|
||||
const double radius = scales.radius.value();
|
||||
const std::size_t density_row = layout.template offset<Kind::residual, Density>();
|
||||
for (const int row : boundary.reduced_dofs()) {
|
||||
CHECK(coordinates.residual_factors()[density_row + row] == Catch::Approx(1.0 / scales.density.value()).epsilon(2.0e-13));
|
||||
}
|
||||
const auto& density_metric = evaluator.cache().template get<normalization::ReferenceVolumeMetric<Density>>();
|
||||
const auto& selected_rows = boundary.reduced_dofs();
|
||||
int interior = 0;
|
||||
while (std::find(selected_rows.begin(), selected_rows.end(), interior) != selected_rows.end()) {
|
||||
++interior;
|
||||
}
|
||||
REQUIRE(interior < static_cast<int>(state.template get<Density>().size()));
|
||||
// The density balance residual is a barotropic pressure potential, a specific energy.
|
||||
CHECK(coordinates.residual_factors()[density_row + interior] == Catch::Approx(1.0 /
|
||||
(scales.specific_energy.value() * std::sqrt(density_metric[interior] * radius * radius * radius))).epsilon(2.0e-13));
|
||||
CHECK(coordinates.state_factors()[layout.template offset<Kind::value, Constant>()] == Catch::Approx(1.0 / scales.specific_energy.value()).epsilon(2.0e-13));
|
||||
const auto infinity = serif::discretization::backend::mfem::select_boundary_dofs<
|
||||
serif::discretization::domain::InfinitySurfaceBoundary>(
|
||||
evaluator.binding().discretization->template binding<Potential>());
|
||||
for (const int row : infinity.reduced_dofs()) {
|
||||
CHECK(coordinates.residual_factors()[layout.template offset<Kind::residual, Potential>() + row] ==
|
||||
Catch::Approx(1.0 / scales.specific_energy.value()).epsilon(2.0e-13));
|
||||
}
|
||||
CHECK(coordinates.residual_factors()[layout.template offset<Kind::residual, Constant>()] == Catch::Approx(1.0 / scales.mass.value()).epsilon(2.0e-13));
|
||||
CHECK(coordinates.residual_factors()[layout.template offset<Kind::residual, Omega>()] == Catch::Approx(1.0 / scales.angular_momentum.value()).epsilon(2.0e-13));
|
||||
const auto& surface_metric = evaluator.cache().template get<deformation::keys::SurfaceMetric>();
|
||||
for (std::size_t mode = 0; mode < state.template get<Shape>().size(); ++mode) {
|
||||
CHECK(coordinates.state_factors()[layout.template offset<Kind::value, Shape>() + mode] == Catch::Approx(std::sqrt(surface_metric[mode]) / (radius * radius)).epsilon(2.0e-13));
|
||||
CHECK(coordinates.residual_factors()[layout.template offset<Kind::residual, ShapeRow>() + mode] == Catch::Approx(1.0 / scales.specific_energy.value()).epsilon(2.0e-13));
|
||||
}
|
||||
|
||||
const Vector seed = tests::deterministic(static_cast<int>(state.values().size()), 0.43);
|
||||
const Vector dual = tests::deterministic(static_cast<int>(coordinates.residual_size()), 1.17);
|
||||
Vector transpose;
|
||||
frozen->jacobian().MultTranspose(dual, transpose);
|
||||
for (const int selected : {-1, 0, 1}) {
|
||||
CAPTURE(selected);
|
||||
Vector direction(seed);
|
||||
if (selected >= 0) {
|
||||
direction = 0.0;
|
||||
const std::size_t offset = selected == 0 ? layout.template offset<Kind::value, Shape>() : layout.template offset<Kind::value, Constant>();
|
||||
const std::size_t count = selected == 0 ? state.template get<Shape>().size() : 1;
|
||||
std::copy_n(seed.GetData() + offset, count, direction.GetData() + offset);
|
||||
}
|
||||
Vector physical_direction(static_cast<int>(state.values().size())), action, physical_action, expected_action(static_cast<int>(coordinates.residual_size()));
|
||||
coordinates.denormalize_state({direction.GetData(), coordinates.state_size()}, {physical_direction.GetData(), coordinates.state_size()});
|
||||
frozen->jacobian().Mult(direction, action);
|
||||
frozen->physical()->jacobian().action()->Mult(physical_direction, physical_action);
|
||||
coordinates.normalize_residual({physical_action.GetData(), coordinates.residual_size()}, {expected_action.GetData(), coordinates.residual_size()});
|
||||
check_rows(action, expected_action, layout, 2.0e-13, 2.0e-13);
|
||||
CHECK((action * dual) == Catch::Approx(direction * transpose).epsilon(3.0e-12).margin(3.0e-12));
|
||||
constexpr double step = 2.0e-6;
|
||||
const auto plus = evaluator.evaluate(perturbed_state(state, step, physical_direction));
|
||||
REQUIRE(plus);
|
||||
CHECK(evaluator.cache().report().template status<normalization::keys::Scales>() == dependencies::PreparationStatus::reused);
|
||||
CHECK(evaluator.cache().report().template status<normalization::keys::Normalization>() == dependencies::PreparationStatus::reused);
|
||||
CHECK(evaluator.cache().report().template status<deformation::keys::SurfaceMetric>() == dependencies::PreparationStatus::reused);
|
||||
Vector difference = residual(**plus);
|
||||
const auto minus = evaluator.evaluate(perturbed_state(state, -step, physical_direction));
|
||||
REQUIRE(minus);
|
||||
difference -= residual(**minus);
|
||||
difference /= 2.0 * step;
|
||||
check_rows(action, difference, layout, 3.0e-6, 3.0e-8);
|
||||
}
|
||||
const auto restored = evaluator.evaluate(state);
|
||||
REQUIRE(restored);
|
||||
check_rows(residual(**restored), residual(*frozen), layout, 0.0, 0.0);
|
||||
Vector repeated;
|
||||
frozen->jacobian().MultTranspose(dual, repeated);
|
||||
CHECK(tests::relative_error(repeated, transpose) == 0.0);
|
||||
}
|
||||
|
||||
/** @brief Check the dimensional inverse, solver-coordinate adapter and independently measured linear residual. */
|
||||
template <typename Prepared, typename Layout>
|
||||
void check_stellar_linear_solve(const Prepared& prepared, const Layout& layout,
|
||||
const serif::discretization::backend::mfem::DofSelection& boundary) {
|
||||
const auto& system = *prepared.system();
|
||||
const auto& coordinates = system.coordinates();
|
||||
const int size = system.jacobian().Width();
|
||||
REQUIRE(size == system.jacobian().Height());
|
||||
REQUIRE(coordinates.state_factors().front() != 1.0);
|
||||
REQUIRE(coordinates.residual_factors().front() != 1.0);
|
||||
CHECK(prepared.contract() == serif::solver::preconditioner::ApplicationContract::fixed_linear);
|
||||
const Vector normalized_direction = tests::deterministic(size, 0.37);
|
||||
Vector physical_direction(size), physical_rhs, normalized_rhs(size);
|
||||
coordinates.denormalize_state({normalized_direction.GetData(), coordinates.state_size()},
|
||||
{physical_direction.GetData(), coordinates.state_size()});
|
||||
system.physical()->jacobian().action()->Mult(physical_direction, physical_rhs);
|
||||
coordinates.normalize_residual({physical_rhs.GetData(), coordinates.residual_size()},
|
||||
{normalized_rhs.GetData(), coordinates.residual_size()});
|
||||
|
||||
Vector physical_correction(size), expected_correction(size), normalized_correction(size), preconditioned_action;
|
||||
prepared.physical_preconditioner().Mult(physical_rhs, physical_correction);
|
||||
coordinates.normalize_state({physical_correction.GetData(), coordinates.state_size()},
|
||||
{expected_correction.GetData(), coordinates.state_size()});
|
||||
prepared.preconditioner().Mult(normalized_rhs, normalized_correction);
|
||||
CHECK(tests::relative_error(normalized_correction, expected_correction) < 3.0e-13);
|
||||
system.physical()->jacobian().action()->Mult(physical_correction, preconditioned_action);
|
||||
for (const int row : boundary.reduced_dofs()) {
|
||||
const int index = static_cast<int>(layout.template offset<Kind::residual, Density>()) + row;
|
||||
CHECK(preconditioned_action[index] == Catch::Approx(physical_rhs[index]).epsilon(2.0e-11).margin(2.0e-12));
|
||||
}
|
||||
// The joint surface/scalar Schur solve must enforce every border equation and mutual coupling.
|
||||
for (const std::size_t index : {layout.template offset<Kind::residual, Constant>(), layout.template offset<Kind::residual, Omega>()}) {
|
||||
CHECK(preconditioned_action[static_cast<int>(index)] == Catch::Approx(physical_rhs[static_cast<int>(index)]).epsilon(2.0e-11).margin(2.0e-12));
|
||||
}
|
||||
|
||||
for (std::size_t mode = 0; mode < layout.template size<Kind::residual, ShapeRow>(); ++mode) {
|
||||
const int index = static_cast<int>(layout.template offset<Kind::residual, ShapeRow>() + mode);
|
||||
CHECK(preconditioned_action[index] == Catch::Approx(physical_rhs[index]).epsilon(2.0e-11).margin(2.0e-12));
|
||||
}
|
||||
|
||||
mfem::FGMRESSolver krylov;
|
||||
krylov.SetRelTol(1.0e-10);
|
||||
krylov.SetAbsTol(1.0e-13);
|
||||
krylov.SetMaxIter(400);
|
||||
krylov.SetKDim(100);
|
||||
krylov.SetPrintLevel(0);
|
||||
krylov.SetPreconditioner(prepared.preconditioner());
|
||||
krylov.SetOperator(system.jacobian());
|
||||
for (const bool newton_right_hand_side : {false, true}) {
|
||||
CAPTURE(newton_right_hand_side);
|
||||
Vector solve_rhs(physical_rhs);
|
||||
if (newton_right_hand_side) {
|
||||
solve_rhs = residual(*system.physical());
|
||||
solve_rhs *= -1.0;
|
||||
}
|
||||
coordinates.normalize_residual({solve_rhs.GetData(), coordinates.residual_size()},
|
||||
{normalized_rhs.GetData(), coordinates.residual_size()});
|
||||
Vector solution(size);
|
||||
solution = 0.0;
|
||||
krylov.Mult(normalized_rhs, solution);
|
||||
CAPTURE(krylov.GetNumIterations(), krylov.GetFinalNorm());
|
||||
CHECK(krylov.GetConverged());
|
||||
coordinates.denormalize_state({solution.GetData(), coordinates.state_size()},
|
||||
{physical_correction.GetData(), coordinates.state_size()});
|
||||
system.physical()->jacobian().action()->Mult(physical_correction, preconditioned_action);
|
||||
Vector measured_residual(preconditioned_action);
|
||||
measured_residual -= solve_rhs;
|
||||
Vector normalized_residual(size);
|
||||
coordinates.normalize_residual({measured_residual.GetData(), coordinates.residual_size()},
|
||||
{normalized_residual.GetData(), coordinates.residual_size()});
|
||||
CHECK(normalized_residual.Norml2() / normalized_rhs.Norml2() < 2.0e-10);
|
||||
CHECK(measured_residual.Norml2() / solve_rhs.Norml2() < 2.0e-8);
|
||||
check_rows(preconditioned_action, solve_rhs, layout, 2.0e-7, 2.0e-9);
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Prepare the production stellar recipe selectively and retain its complete frozen numerical state. */
|
||||
void check_moving_preconditioner() {
|
||||
namespace normalization = serif::solver::normalization;
|
||||
namespace preconditioner = serif::solver::preconditioner;
|
||||
using InverseKey = preconditioner::keys::Preconditioner;
|
||||
using PreparedKey = model::preconditioning::PreparedSystem;
|
||||
using Status = dependencies::PreparationStatus;
|
||||
#ifdef MFEM_USE_MPI
|
||||
// The AMG backend borrows the application's MPI session; the test is its application owner.
|
||||
if (!mfem::Mpi::IsInitialized()) {
|
||||
mfem::Mpi::Init();
|
||||
}
|
||||
#endif
|
||||
auto fixture = std::make_shared<Fixture>(mfem::Ordering::byNODES);
|
||||
std::weak_ptr<Fixture> lifetime = fixture;
|
||||
Vector right_hand_side, frozen_correction, frozen_action;
|
||||
auto retained = [&]() {
|
||||
const auto physics = physical_model();
|
||||
const auto discretization = discretize(fixture);
|
||||
using Description = decltype(model::resolve_equilibrium_equations(physics));
|
||||
using Plan = model::EquilibriumPlan<Polytrope, Description>;
|
||||
const std::array shape{0.004, -0.002, 0.001, 0.003, -0.001, 0.002, -0.003, 0.001, 0.002};
|
||||
const auto layout = backend::equilibrium_layout<Plan>(discretization, shape);
|
||||
const auto state = initial_state(layout, fixture->fields, shape);
|
||||
const auto recipe = preconditioner::make_primal_preconditioner(layout);
|
||||
static_assert(dependencies::ContainsKey<Density, typename decltype(recipe)::Values>);
|
||||
static_assert(dependencies::ContainsKey<Potential, typename decltype(recipe)::Values>);
|
||||
static_assert(dependencies::ContainsKey<Shape, typename decltype(recipe)::Values>);
|
||||
static_assert(!dependencies::ContainsKey<::serif::tests::equations::Enthalpy, typename Plan::Values>);
|
||||
const auto configured = ::serif::model::make_discretization(::serif::model::DiscretizationOptions{
|
||||
.model = physics, .finite_elements = discretization,
|
||||
.normalization = normalization::make_normalization(normalization::NormalizationOptions{
|
||||
.scheme = normalization::LaneEmdenApproximation{.n = 3.0, .central_density = serif::dimensions::DensityValue{0.9}}, .coordinates = normalization::Dimensionless{},
|
||||
.gravitational_constant = serif::dimensions::QuantityValue<serif::dimensions::GravitationalConstant>{2.3 / (4.0 * std::numbers::pi)}
|
||||
}),
|
||||
.preconditioner = recipe
|
||||
});
|
||||
serif::discretization::quadrature::RuleSet rules;
|
||||
rules.set_fallback_control({.fixed_order = 8});
|
||||
const execution::VolumeOptions volume_options{.quadrature_policy = serif::discretization::quadrature::Policy{rules}};
|
||||
auto evaluator = backend::bind_stellar_model(configured, state, volume_options);
|
||||
const auto physical = evaluator.evaluate(state);
|
||||
REQUIRE(physical);
|
||||
CHECK(evaluator.cache().report().template status<InverseKey>() == Status::not_required);
|
||||
CHECK(evaluator.cache().report().template status<PreparedKey>() == Status::not_required);
|
||||
const auto prepared = evaluator.prepare_linear_system(state);
|
||||
if (!prepared) {
|
||||
UNSCOPED_INFO("Failed computation index: " << prepared.error().computation_index);
|
||||
if (std::holds_alternative<preconditioner::Failure>(prepared.error().error)) {
|
||||
UNSCOPED_INFO(std::get<preconditioner::Failure>(prepared.error().error).message);
|
||||
}
|
||||
}
|
||||
REQUIRE(prepared);
|
||||
const auto first = *prepared;
|
||||
CHECK(first->system() == *physical);
|
||||
CHECK(evaluator.cache().report().template status<model::keys::PhysicalSystem>() == Status::reused);
|
||||
CHECK(evaluator.cache().report().template status<InverseKey>() == Status::computed);
|
||||
const auto repeated = evaluator.prepare_linear_system(state);
|
||||
REQUIRE(repeated);
|
||||
CHECK(*repeated == first);
|
||||
CHECK(evaluator.cache().report().computed_count() == 0);
|
||||
CHECK(evaluator.cache().report().template status<InverseKey>() == Status::reused);
|
||||
|
||||
const auto boundary = serif::discretization::backend::mfem::select_boundary_dofs<
|
||||
serif::discretization::domain::StellarSurfaceBoundary>(fixture->fields.density_binding);
|
||||
check_stellar_linear_solve(*first, layout, boundary);
|
||||
right_hand_side = tests::deterministic(static_cast<int>(state.values().size()), 0.82);
|
||||
frozen_correction.SetSize(right_hand_side.Size());
|
||||
first->preconditioner().Mult(right_hand_side, frozen_correction);
|
||||
first->system()->jacobian().Mult(right_hand_side, frozen_action);
|
||||
const Vector first_residual = residual(*first->system());
|
||||
|
||||
auto candidate = state;
|
||||
candidate.template get<Density>()[0] += 0.01;
|
||||
candidate.template get<Shape>()[0] += 0.0002;
|
||||
const auto trial = evaluator.evaluate(candidate);
|
||||
REQUIRE(trial);
|
||||
CHECK(*trial != first->system());
|
||||
CHECK(evaluator.cache().report().template status<InverseKey>() == Status::not_required);
|
||||
CHECK(evaluator.cache().report().template status<PreparedKey>() == Status::not_required);
|
||||
CHECK(tests::relative_error(residual(*first->system()), first_residual) == 0.0);
|
||||
const auto updated = evaluator.prepare_linear_system(candidate);
|
||||
REQUIRE(updated);
|
||||
CHECK(*updated != first);
|
||||
CHECK((*updated)->system() == *trial);
|
||||
CHECK(evaluator.cache().report().template status<InverseKey>() == Status::computed);
|
||||
const auto rebuilt = evaluator.prepare_linear_system(candidate, dependencies::make_dependency_context(), dependencies::PreparationMode::rebuild);
|
||||
REQUIRE(rebuilt);
|
||||
CHECK(*rebuilt != *updated);
|
||||
CHECK(evaluator.cache().report().template status<model::keys::PhysicalSystem>() == Status::computed);
|
||||
CHECK(evaluator.cache().report().template status<InverseKey>() == Status::computed);
|
||||
Vector repeated_correction(right_hand_side.Size()), repeated_action;
|
||||
first->preconditioner().Mult(right_hand_side, repeated_correction);
|
||||
first->system()->jacobian().Mult(right_hand_side, repeated_action);
|
||||
CHECK(tests::relative_error(repeated_correction, frozen_correction) == 0.0);
|
||||
CHECK(tests::relative_error(repeated_action, frozen_action) == 0.0);
|
||||
|
||||
// At n=1.5 the exact density Jacobian is undefined at zero density; reject the EOS trial.
|
||||
auto singular = state;
|
||||
std::ranges::fill(singular.template get<Density>(), 0.0);
|
||||
singular.template get<Omega>()[0] = 0.0;
|
||||
const auto preparations = evaluator.cache().preparation_count();
|
||||
const auto rejected = evaluator.prepare_linear_system(singular);
|
||||
REQUIRE_FALSE(rejected);
|
||||
REQUIRE(std::holds_alternative<execution::PreparationRejection>(rejected.error().error));
|
||||
CHECK(std::get<execution::PreparationRejection>(rejected.error().error).reason == execution::PreparationRejectionReason::EQUATION);
|
||||
CHECK_FALSE(evaluator.cache().ready());
|
||||
CHECK_FALSE(evaluator.cache().template contains<PreparedKey>());
|
||||
CHECK(evaluator.cache().report().computed_count() == 0);
|
||||
CHECK(evaluator.cache().report().reused_count() == 0);
|
||||
CHECK(evaluator.cache().preparation_count() == preparations);
|
||||
first->preconditioner().Mult(right_hand_side, repeated_correction);
|
||||
first->system()->jacobian().Mult(right_hand_side, repeated_action);
|
||||
CHECK(tests::relative_error(repeated_correction, frozen_correction) == 0.0);
|
||||
CHECK(tests::relative_error(repeated_action, frozen_action) == 0.0);
|
||||
CHECK(tests::relative_error(residual(*first->system()), first_residual) == 0.0);
|
||||
const auto recovered = evaluator.prepare_linear_system(candidate);
|
||||
REQUIRE(recovered);
|
||||
CHECK(evaluator.cache().ready());
|
||||
return first;
|
||||
}();
|
||||
fixture.reset();
|
||||
CHECK_FALSE(lifetime.expired());
|
||||
Vector repeated_correction(right_hand_side.Size()), repeated_action;
|
||||
retained->preconditioner().Mult(right_hand_side, repeated_correction);
|
||||
retained->system()->jacobian().Mult(right_hand_side, repeated_action);
|
||||
CHECK(tests::relative_error(repeated_correction, frozen_correction) == 0.0);
|
||||
CHECK(tests::relative_error(repeated_action, frozen_action) == 0.0);
|
||||
retained.reset();
|
||||
CHECK(lifetime.expired());
|
||||
}
|
||||
|
||||
/** @brief Evaluate the full moving star in both native displacement orderings and retain its frozen actions. */
|
||||
template <bool Normalize = false>
|
||||
void check_moving_equilibrium(const mfem::Ordering::Type ordering) {
|
||||
auto fixture = std::make_shared<Fixture>(ordering);
|
||||
std::weak_ptr<Fixture> lifetime = fixture;
|
||||
Vector direction, dual, action, transpose, normalized_action, normalized_transpose;
|
||||
std::shared_ptr<const backend::NormalizedSystem> normalized_retained;
|
||||
const auto exercise = [&]() {
|
||||
auto& fields = fixture->fields;
|
||||
const auto physics = physical_model();
|
||||
const auto discretization = discretize(fixture);
|
||||
using Description = decltype(model::resolve_equilibrium_equations(physics));
|
||||
using Plan = model::EquilibriumPlan<Polytrope, Description>;
|
||||
const std::array shape{0.004, -0.002, 0.001, 0.003, -0.001, 0.002, -0.003, 0.001, 0.002};
|
||||
const auto layout = backend::equilibrium_layout<Plan>(discretization, shape);
|
||||
REQUIRE(layout.state_sizes.size() == 5);
|
||||
REQUIRE(layout.residual_sizes.size() == 5);
|
||||
REQUIRE(layout.template total_size<Kind::value>() == layout.template total_size<Kind::residual>());
|
||||
REQUIRE(shape.size() < static_cast<std::size_t>(fields.displacement_binding.size()));
|
||||
const auto state = initial_state(layout, fields, shape);
|
||||
const auto boundary = serif::discretization::backend::mfem::select_boundary_dofs<
|
||||
serif::discretization::domain::StellarSurfaceBoundary>(fields.density_binding);
|
||||
serif::discretization::quadrature::RuleSet rules;
|
||||
rules.set_fallback_control({.fixed_order = 8});
|
||||
const execution::VolumeOptions volume_options{.quadrature_policy = serif::discretization::quadrature::Policy{rules}};
|
||||
auto evaluator = [&]() {
|
||||
if constexpr (Normalize) {
|
||||
namespace normalization = serif::solver::normalization;
|
||||
using Reference = normalization::LaneEmdenApproximation<normalization::FromModel, serif::dimensions::DensityValue>;
|
||||
const auto configured = ::serif::model::make_discretization(::serif::model::DiscretizationOptions{
|
||||
.model = physics, .finite_elements = discretization,
|
||||
.normalization = normalization::make_normalization(normalization::NormalizationOptions<Reference>{
|
||||
.scheme = Reference{.n = 3.0, .central_density = serif::dimensions::DensityValue{0.9}},
|
||||
.coordinates = normalization::Dimensionless{},
|
||||
.gravitational_constant = serif::dimensions::QuantityValue<serif::dimensions::GravitationalConstant>{2.3 / (4.0 * std::numbers::pi)}
|
||||
})
|
||||
});
|
||||
return backend::bind_stellar_model(configured, state, volume_options);
|
||||
} else {
|
||||
return backend::bind_stellar_model(physics, discretization, state, volume_options);
|
||||
}
|
||||
}();
|
||||
const auto initial = evaluator.evaluate(state);
|
||||
REQUIRE(initial);
|
||||
const auto first = *initial;
|
||||
using MassRow = model::computations::ScalarResidualRow<Constant>;
|
||||
using AngularMomentumRow = model::computations::ScalarResidualRow<Omega>;
|
||||
const auto mass_row = evaluator.cache().template share<MassRow>();
|
||||
const auto angular_momentum_row = evaluator.cache().template share<AngularMomentumRow>();
|
||||
CHECK(evaluator.cache().report().template status<MassRow>() == dependencies::PreparationStatus::computed);
|
||||
CHECK(evaluator.cache().report().template status<AngularMomentumRow>() == dependencies::PreparationStatus::computed);
|
||||
const auto repeated = evaluator.evaluate(state);
|
||||
REQUIRE(repeated);
|
||||
CHECK(*repeated == first);
|
||||
CHECK(evaluator.cache().report().computed_count() == 0);
|
||||
CHECK(evaluator.cache().template share<MassRow>() == mass_row);
|
||||
CHECK(evaluator.cache().template share<AngularMomentumRow>() == angular_momentum_row);
|
||||
|
||||
// A successful trial is merely another result; the caller retains the previous candidate.
|
||||
const auto original = state;
|
||||
auto copied = state;
|
||||
CHECK(copied.values().data() != state.values().data());
|
||||
copied.template get<Density>()[0] += 0.01;
|
||||
copied.template get<Shape>()[0] += 0.0002;
|
||||
auto moved = std::move(copied);
|
||||
CHECK(std::ranges::equal(state.values(), original.values()));
|
||||
const Vector first_residual = residual(*first);
|
||||
const auto discarded = evaluator.evaluate(moved);
|
||||
REQUIRE(discarded);
|
||||
CHECK(evaluator.cache().template share<MassRow>() != mass_row);
|
||||
CHECK(evaluator.cache().template share<AngularMomentumRow>() != angular_momentum_row);
|
||||
CHECK(evaluator.cache().report().template status<MassRow>() == dependencies::PreparationStatus::computed);
|
||||
CHECK(evaluator.cache().report().template status<AngularMomentumRow>() == dependencies::PreparationStatus::computed);
|
||||
CHECK(*discarded != first);
|
||||
CHECK(tests::relative_error(residual(**discarded), first_residual) > 1.0e-10);
|
||||
CHECK(tests::relative_error(residual(*first), first_residual) == 0.0);
|
||||
CHECK(std::ranges::equal(state.values(), original.values()));
|
||||
const auto restored = evaluator.evaluate(state);
|
||||
REQUIRE(restored);
|
||||
if constexpr (Normalize) {
|
||||
check_normalized_system(evaluator, state, boundary);
|
||||
} else {
|
||||
check_composition(evaluator, state, **restored, boundary);
|
||||
check_scalar_row<Constant>(evaluator, **restored);
|
||||
check_scalar_row<Omega>(evaluator, **restored);
|
||||
check_derivatives(evaluator, state);
|
||||
}
|
||||
|
||||
const auto evaluated = evaluator.evaluate(state);
|
||||
REQUIRE(evaluated);
|
||||
const auto retained = *evaluated;
|
||||
const auto physical = [&]() {
|
||||
if constexpr (Normalize) {
|
||||
return retained->physical();
|
||||
} else {
|
||||
return retained;
|
||||
}
|
||||
}();
|
||||
const auto preparations = evaluator.cache().preparation_count();
|
||||
const Vector retained_residual = residual(*retained);
|
||||
direction = tests::deterministic(static_cast<int>(state.values().size()), 0.61);
|
||||
dual = tests::deterministic(static_cast<int>(state.values().size()), 1.31);
|
||||
physical->jacobian().action()->Mult(direction, action);
|
||||
physical->jacobian().action()->MultTranspose(dual, transpose);
|
||||
if constexpr (Normalize) {
|
||||
normalized_retained = retained;
|
||||
normalized_retained->jacobian().Mult(direction, normalized_action);
|
||||
normalized_retained->jacobian().MultTranspose(dual, normalized_transpose);
|
||||
}
|
||||
auto invalid = state;
|
||||
invalid.template get<Shape>()[0] = std::numeric_limits<double>::quiet_NaN();
|
||||
const auto rejected = evaluator.evaluate(invalid);
|
||||
REQUIRE_FALSE(rejected);
|
||||
CHECK(std::holds_alternative<deformation::DeformationFailure>(rejected.error().error));
|
||||
CHECK(tests::relative_error(residual(*retained), retained_residual) == 0.0);
|
||||
if constexpr (Normalize) {
|
||||
CHECK_FALSE(evaluator.cache().template contains<serif::model::normalization::PreparedSystem>());
|
||||
}
|
||||
CHECK_FALSE(evaluator.cache().ready());
|
||||
CHECK_FALSE(evaluator.cache().template contains<model::keys::PhysicalSystem>());
|
||||
CHECK(evaluator.cache().report().computed_count() == 0);
|
||||
CHECK(evaluator.cache().report().reused_count() == 0);
|
||||
CHECK(evaluator.cache().preparation_count() == preparations);
|
||||
const auto recovered = evaluator.evaluate(state);
|
||||
REQUIRE(recovered);
|
||||
CHECK(*recovered == retained);
|
||||
CHECK(evaluator.cache().report().computed_count() == 0);
|
||||
return physical;
|
||||
};
|
||||
auto retained = exercise();
|
||||
fixture.reset();
|
||||
CHECK_FALSE(lifetime.expired());
|
||||
Vector repeated_action, repeated_transpose;
|
||||
retained->jacobian().action()->Mult(direction, repeated_action);
|
||||
retained->jacobian().action()->MultTranspose(dual, repeated_transpose);
|
||||
CHECK(tests::relative_error(repeated_action, action) == 0.0);
|
||||
CHECK(tests::relative_error(repeated_transpose, transpose) == 0.0);
|
||||
retained.reset();
|
||||
if constexpr (Normalize) {
|
||||
CHECK_FALSE(lifetime.expired());
|
||||
normalized_retained->jacobian().Mult(direction, repeated_action);
|
||||
normalized_retained->jacobian().MultTranspose(dual, repeated_transpose);
|
||||
CHECK(tests::relative_error(repeated_action, normalized_action) == 0.0);
|
||||
CHECK(tests::relative_error(repeated_transpose, normalized_transpose) == 0.0);
|
||||
normalized_retained.reset();
|
||||
}
|
||||
CHECK(lifetime.expired());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Complete moving stellar assembly preserves every coupled equation and boundary row", "[model][moving_equilibrium][integration]") {
|
||||
SECTION("Component-block displacement ordering") {
|
||||
check_moving_equilibrium(mfem::Ordering::byNODES);
|
||||
}
|
||||
SECTION("Interleaved displacement ordering") {
|
||||
check_moving_equilibrium(mfem::Ordering::byVDIM);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Complete moving stellar normalization preserves solver coordinates and frozen ownership", "[model][moving_normalization][integration]") {
|
||||
check_moving_equilibrium<true>(mfem::Ordering::byNODES);
|
||||
}
|
||||
|
||||
TEST_CASE("Complete moving stellar preconditioning prepares a frozen solver system and solves its physical Jacobian", "[model][moving_preconditioner][integration]") {
|
||||
#ifdef MFEM_USE_MPI
|
||||
check_moving_preconditioner();
|
||||
#else
|
||||
SKIP("The default stellar recipe uses the one-process BoomerAMG backend, which requires an MPI-enabled MFEM build.");
|
||||
#endif
|
||||
}
|
||||
Reference in New Issue
Block a user