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:
427
tests/equations/angular_momentum.cpp
Normal file
427
tests/equations/angular_momentum.cpp
Normal file
@@ -0,0 +1,427 @@
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <catch2/catch_approx.hpp>
|
||||
|
||||
#include "serif/equations/angular_momentum.hpp"
|
||||
#include "serif/equations/barotropic_density.hpp"
|
||||
#include "serif/equations/primal_gravity.hpp"
|
||||
#include "serif/equations/mass_normalization.hpp"
|
||||
#include "serif/tests/equations/gravity_port.hpp"
|
||||
|
||||
namespace {
|
||||
using namespace serif;
|
||||
using namespace serif::tests::equations;
|
||||
using namespace serif::tests::equations::gravity;
|
||||
using Equation = serif::equations::AngularMomentumConstraint;
|
||||
using Prepared = VolumeEquationFixture<Equation, Schema>;
|
||||
using Vector = ::mfem::Vector;
|
||||
using Axis = serif::equations::RigidRotation::Vector;
|
||||
|
||||
struct Values {
|
||||
Vector density, omega, displacement;
|
||||
};
|
||||
|
||||
auto state(GravityFixture& f, const Values& values) {
|
||||
return backend::make_term_coefficients(backend::bind(f.density_binding, values.density), backend::bind(f.angular_velocity_binding, values.omega));
|
||||
}
|
||||
|
||||
auto make_operator(GravityFixture& f, double target, Axis axis = {0.0, 0.0, 1.0}, Axis center = {}) {
|
||||
return Prepared{Equation{target, axis, center}, f.angular_velocity_binding, f.displacement_binding, f.compactification, f.mapper, reference_policy()};
|
||||
}
|
||||
|
||||
template <typename OperatorT> double residual(GravityFixture& f, OperatorT& op) {
|
||||
Vector result;
|
||||
op.residual(f.workspace, result);
|
||||
REQUIRE(result.Size() == 1);
|
||||
return result[0];
|
||||
}
|
||||
|
||||
template <typename OperatorT> double residual(GravityFixture& f, OperatorT& op, const Values& values) {
|
||||
op.prepare(state(f, values), values.displacement, f.workspace);
|
||||
return residual(f, op);
|
||||
}
|
||||
|
||||
template <typename OperatorT> double action(GravityFixture& f, OperatorT& op, const Values& direction) {
|
||||
Vector result;
|
||||
op.apply_jacobian(state(f, direction), direction.displacement, f.workspace, result);
|
||||
REQUIRE(result.Size() == 1);
|
||||
return result[0];
|
||||
}
|
||||
|
||||
Values zeros(const GravityFixture& f) {
|
||||
return {constant(f.density_binding.size(), 0.0), constant(1, 0.0), constant(f.displacement_binding.size(), 0.0)};
|
||||
}
|
||||
|
||||
// MeanField tests/operators/prepared_angular_momentum.cpp: preserve its projected profiles.
|
||||
Vector density(GravityFixture& f, double phase, bool direction = false) {
|
||||
return f.project(f.density_binding, [=](const Vector& x) {
|
||||
if (direction) { return 0.17 * std::sin(0.83 * x[0] + phase) - 0.12 * std::cos(0.79 * x[1] - phase) + 0.06 * x[2]; }
|
||||
return 0.94 + 0.08 * std::sin(0.71 * x[0] + phase) + 0.05 * std::cos(0.63 * x[1] - phase) + 0.03 * x[2] * x[2];
|
||||
});
|
||||
}
|
||||
|
||||
Vector displacement(GravityFixture& f, double scale) {
|
||||
return gravity_port::project_vector(f.displacement_binding, [=](const Vector& x, Vector& value) {
|
||||
value.SetSize(3);
|
||||
value[0] = scale * (0.07 * x[0] + 0.018 * x[1] * x[2]);
|
||||
value[1] = scale * (-0.05 * x[1] + 0.013 * x[0] * x[2]);
|
||||
value[2] = scale * (0.04 * x[2] - 0.011 * x[0] * x[1]);
|
||||
});
|
||||
}
|
||||
|
||||
double relative(double actual, double expected) {
|
||||
return std::abs(actual - expected) / std::max({std::abs(actual), std::abs(expected), 100.0 * std::numeric_limits<double>::epsilon()});
|
||||
}
|
||||
|
||||
Values shifted(Values values, const Values& direction, double step) {
|
||||
values.density.Add(step, direction.density);
|
||||
values.omega.Add(step, direction.omega);
|
||||
values.displacement.Add(step, direction.displacement);
|
||||
return values;
|
||||
}
|
||||
|
||||
struct Integrals {
|
||||
double moment = 0.0, mass = 0.0, action = 0.0;
|
||||
Axis first_moment{};
|
||||
};
|
||||
|
||||
// Direct MFEM quadrature, independent of the equation and operator evaluators.
|
||||
// delta(I) = integral [delta(rho) s^2 + 2 rho r_perp . delta(x) + rho s^2 trace(A^-1 delta(A))] dV.
|
||||
Integrals reference(GravityFixture& f, const Values& values, Axis axis = {0.0, 0.0, 1.0}, Axis center = {}, const Values* direction = nullptr) {
|
||||
const double norm = std::hypot(axis[0], axis[1], axis[2]);
|
||||
for (double& component : axis) { component /= norm; }
|
||||
::mfem::GridFunction rho(&f.density_space), u(&f.displacement_space), drho(&f.density_space), du(&f.displacement_space);
|
||||
rho = f.density_binding.scatter(values.density);
|
||||
u = f.displacement_binding.scatter(values.displacement);
|
||||
if (direction) {
|
||||
drho = f.density_binding.scatter(direction->density);
|
||||
du = f.displacement_binding.scatter(direction->displacement);
|
||||
}
|
||||
Integrals result;
|
||||
for (int element = 0; element < f.mesh.GetNE(); ++element) {
|
||||
if (!f.density_binding.supports(element)) { continue; }
|
||||
auto& transform = *f.mesh.GetElementTransformation(element);
|
||||
const auto& rule = ::mfem::IntRules.Get(f.mesh.GetElementGeometry(element), reference_order);
|
||||
for (int point = 0; point < rule.GetNPoints(); ++point) {
|
||||
const auto& ip = rule.IntPoint(point);
|
||||
transform.SetIntPoint(&ip);
|
||||
Vector x(3), displacement_value(3), dx(3);
|
||||
transform.Transform(ip, x);
|
||||
u.GetVectorValue(transform, ip, displacement_value);
|
||||
x += displacement_value;
|
||||
::mfem::DenseMatrix A(3), dA(3), inverse(3);
|
||||
u.GetVectorGradient(transform, A);
|
||||
for (int i = 0; i < 3; ++i) { A(i, i) += 1.0; }
|
||||
double axial_distance = 0.0;
|
||||
for (int i = 0; i < 3; ++i) { axial_distance += axis[i] * (x[i] - center[i]); }
|
||||
Axis perpendicular{};
|
||||
double radius_squared = 0.0;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
perpendicular[i] = x[i] - center[i] - axis[i] * axial_distance;
|
||||
radius_squared += perpendicular[i] * perpendicular[i];
|
||||
}
|
||||
const double weight = ip.weight * transform.Weight() * A.Det();
|
||||
const double density_value = rho.GetValue(transform, ip);
|
||||
result.mass += weight * density_value;
|
||||
result.moment += weight * density_value * radius_squared;
|
||||
for (int i = 0; i < 3; ++i) { result.first_moment[i] += weight * density_value * x[i]; }
|
||||
if (direction) {
|
||||
du.GetVectorValue(transform, ip, dx);
|
||||
du.GetVectorGradient(transform, dA);
|
||||
::mfem::CalcInverse(A, inverse);
|
||||
double trace = 0.0, radius_variation = 0.0;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
radius_variation += 2.0 * perpendicular[i] * dx[i];
|
||||
for (int j = 0; j < 3; ++j) { trace += inverse(i, j) * dA(j, i); }
|
||||
}
|
||||
result.action += weight * (direction->omega[0] * density_value * radius_squared + values.omega[0] *
|
||||
(drho.GetValue(transform, ip) * radius_squared + density_value * (radius_variation + radius_squared * trace)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
double moment(GravityFixture& f, Prepared& op) {
|
||||
auto direction = zeros(f);
|
||||
direction.omega = 1.0;
|
||||
return action(f, op, direction);
|
||||
}
|
||||
|
||||
void check_columns(GravityFixture& f, Axis axis = {0.0, 0.0, 1.0}, Axis center = {}) {
|
||||
CAPTURE(f.mesh.GetNE(), f.mesh.GetElementGeometry(0), f.displacement_space.GetOrdering(), axis, center);
|
||||
const Values values{density(f, 0.31), constant(1, 0.63), displacement(f, 0.43)};
|
||||
const Values direction{density(f, 0.67, true), constant(1, -0.37), displacement(f, -0.79)};
|
||||
auto op = make_operator(f, 0.81, axis, center);
|
||||
(void)residual(f, op, values);
|
||||
const double complete = action(f, op, direction);
|
||||
CHECK(relative(complete, reference(f, values, axis, center, &direction).action) < 2.0e-11);
|
||||
auto density_direction = zeros(f), geometry_direction = zeros(f), omega_direction = zeros(f);
|
||||
density_direction.density = direction.density;
|
||||
geometry_direction.displacement = direction.displacement;
|
||||
omega_direction.omega = direction.omega;
|
||||
const double density_action = action(f, op, density_direction), geometry_action = action(f, op, geometry_direction), omega_action = action(f, op, omega_direction);
|
||||
CHECK(relative(complete, density_action + geometry_action + omega_action) < 3.0e-15);
|
||||
CHECK(omega_action == Catch::Approx(moment(f, op) * direction.omega[0]).epsilon(2.0e-15));
|
||||
backend::JacobianOperator adapter(op.cache());
|
||||
CHECK(adapter.Height() == 1);
|
||||
CHECK(adapter.Width() == direction.density.Size() + 1 + direction.displacement.Size());
|
||||
Vector result;
|
||||
adapter.Mult(packed(direction.density, direction.omega, direction.displacement), result);
|
||||
CHECK(result[0] == complete);
|
||||
|
||||
// Preserve all three original difference steps and thresholds.
|
||||
const std::array<Values, 3> columns{density_direction, geometry_direction, omega_direction};
|
||||
const std::array<double, 3> actions{density_action, geometry_action, omega_action};
|
||||
const std::array<double, 3> steps{1.0e-3, 1.0e-6, 1.0e-6}, tolerances{4.0e-8, 4.0e-7, 2.0e-10};
|
||||
for (int column = 0; column < 3; ++column) {
|
||||
const double plus = residual(f, op, shifted(values, columns[column], steps[column]));
|
||||
const double minus = residual(f, op, shifted(values, columns[column], -steps[column]));
|
||||
const double difference = (plus - minus) / (2.0 * steps[column]);
|
||||
CAPTURE(column, actions[column], difference);
|
||||
CHECK(relative(actions[column], difference) < tolerances[column]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Angular momentum retains moment scaling and the parallel axis theorem", "[equations][angular_momentum][integration]") {
|
||||
STATIC_CHECK_FALSE(std::is_copy_constructible_v<Prepared::Cache>);
|
||||
STATIC_CHECK(std::is_move_constructible_v<Prepared::Cache>);
|
||||
auto fixture = gravity_port::stellar_fixture();
|
||||
auto& f = *fixture;
|
||||
auto values = zeros(f);
|
||||
values.density = 1.37;
|
||||
values.omega = 0.73;
|
||||
auto op = make_operator(f, 0.41, {0.0, 0.0, 4.0});
|
||||
const double initial = residual(f, op, values), origin_moment = moment(f, op);
|
||||
const auto independent = reference(f, values);
|
||||
CHECK(relative(origin_moment, independent.moment) < 2.0e-13);
|
||||
CHECK(initial + 0.41 == Catch::Approx(0.73 * origin_moment).epsilon(2.0e-15));
|
||||
CHECK(initial == Catch::Approx(0.73 * origin_moment - 0.41).epsilon(2.0e-15));
|
||||
values.displacement = gravity_port::affine_displacement(f, 0.086);
|
||||
(void)residual(f, op, values);
|
||||
CHECK(relative(moment(f, op) / independent.moment, std::pow(1.0 + 0.086, 5)) < 7.0e-7);
|
||||
|
||||
values.displacement = 0.0;
|
||||
const Axis center{0.27, -0.19, 0.31};
|
||||
auto shifted_op = make_operator(f, 0.41, {0.0, 0.0, 1.0}, center);
|
||||
(void)residual(f, shifted_op, values);
|
||||
const double expected = origin_moment + independent.mass * (center[0] * center[0] + center[1] * center[1]) -
|
||||
2.0 * (center[0] * independent.first_moment[0] + center[1] * independent.first_moment[1]);
|
||||
CHECK(relative(moment(f, shifted_op), expected) < 3.0e-13);
|
||||
}
|
||||
|
||||
TEST_CASE("Angular momentum retains density geometry and angular velocity action accuracy", "[equations][angular_momentum][integration]") {
|
||||
for (const auto geometry : {::mfem::Element::TETRAHEDRON, ::mfem::Element::HEXAHEDRON}) {
|
||||
for (const auto ordering : {::mfem::Ordering::byNODES, ::mfem::Ordering::byVDIM}) {
|
||||
GravityFixture f(geometry, ordering);
|
||||
f.set_compactification();
|
||||
check_columns(f);
|
||||
check_columns(f, {0.17, -0.12, 0.43}, {0.031, -0.024, 0.018});
|
||||
}
|
||||
}
|
||||
auto fixture = gravity_port::stellar_fixture();
|
||||
check_columns(*fixture);
|
||||
check_columns(*fixture, {0.17, -0.12, 0.43}, {0.031, -0.024, 0.018});
|
||||
}
|
||||
|
||||
TEST_CASE("Angular momentum snapshots and refreshed states preserve the old lifecycle numerics", "[equations][angular_momentum][integration]") {
|
||||
auto fixture = gravity_port::stellar_fixture();
|
||||
auto& f = *fixture;
|
||||
Values values{density(f, 0.29), constant(1, 0.52), displacement(f, 0.41)};
|
||||
const Values direction{density(f, 0.67, true), constant(1, -0.19), displacement(f, -0.53)};
|
||||
auto op = make_operator(f, 0.71);
|
||||
const double initial = residual(f, op, values), initial_moment = moment(f, op);
|
||||
auto check_action = [&] { CHECK(relative(action(f, op, direction), reference(f, values, {0.0, 0.0, 1.0}, {}, &direction).action) < 2.0e-11); };
|
||||
check_action();
|
||||
CHECK(residual(f, op, values) == initial);
|
||||
check_action();
|
||||
auto potential = f.potential();
|
||||
potential *= 1.7;
|
||||
op.prepare(backend::make_term_coefficients(backend::bind(f.density_binding, values.density), backend::bind(f.angular_velocity_binding, values.omega), backend::bind(f.potential_binding, potential)), values.displacement, f.workspace);
|
||||
CHECK(residual(f, op) == initial);
|
||||
check_action();
|
||||
|
||||
values.omega = 0.81;
|
||||
CHECK(residual(f, op) == initial);
|
||||
CHECK(residual(f, op, values) - initial == Catch::Approx((0.81 - 0.52) * initial_moment).epsilon(3.0e-15));
|
||||
CHECK(moment(f, op) == initial_moment);
|
||||
check_action();
|
||||
const double before_density = residual(f, op);
|
||||
values.density = density(f, 0.83);
|
||||
CHECK(residual(f, op) == before_density);
|
||||
(void)residual(f, op, values);
|
||||
check_action();
|
||||
const double before_geometry = residual(f, op);
|
||||
values.displacement = displacement(f, 0.87);
|
||||
CHECK(residual(f, op) == before_geometry);
|
||||
(void)residual(f, op, values);
|
||||
check_action();
|
||||
}
|
||||
|
||||
TEST_CASE("Angular momentum rejects inadmissible moments and angular speeds then recovers", "[equations][angular_momentum][unit]") {
|
||||
GravityFixture f;
|
||||
auto values = zeros(f);
|
||||
values.density = 0.91;
|
||||
values.omega = 0.52;
|
||||
auto op = make_operator(f, 0.71);
|
||||
const double initial = residual(f, op, values);
|
||||
for (const double omega : {0.52, 0.0, -0.52}) {
|
||||
values.omega = omega;
|
||||
values.density = -0.91;
|
||||
const auto count = op.preparation_count();
|
||||
const auto rejected = op.try_prepare(state(f, values), values.displacement, f.workspace);
|
||||
REQUIRE_FALSE(rejected.has_value());
|
||||
CHECK(rejected.error().reason == backend::PreparationRejectionReason::EQUATION);
|
||||
CHECK(op.preparation_count() == count);
|
||||
CHECK_FALSE(op.is_prepared());
|
||||
CHECK_THROWS_AS(op.prepare(state(f, values), values.displacement, f.workspace), std::domain_error);
|
||||
values.density = 0.91;
|
||||
values.omega = 0.52;
|
||||
CHECK(residual(f, op, values) == initial);
|
||||
}
|
||||
for (double bad : {std::numeric_limits<double>::quiet_NaN(), std::numeric_limits<double>::infinity()}) {
|
||||
values.omega = bad;
|
||||
const auto rejected = op.try_prepare(state(f, values), values.displacement, f.workspace);
|
||||
REQUIRE_FALSE(rejected.has_value());
|
||||
CHECK(rejected.error().reason == backend::PreparationRejectionReason::NON_FINITE);
|
||||
CHECK_FALSE(op.is_prepared());
|
||||
CHECK_THROWS_AS(op.prepare(state(f, values), values.displacement, f.workspace), std::domain_error);
|
||||
values.omega = 0.52;
|
||||
CHECK(residual(f, op, values) == initial);
|
||||
}
|
||||
// Local negative density is allowed when the total moment remains nonnegative.
|
||||
values.density = f.project(f.density_binding, [](const Vector& x) { return x[0] - 0.25; });
|
||||
REQUIRE(reference(f, values).moment > 0.0);
|
||||
CHECK(relative(residual(f, op, values), 0.52 * reference(f, values).moment - 0.71) < 2.0e-13);
|
||||
}
|
||||
|
||||
TEST_CASE("Angular momentum target axis and center retain the old validity requirements", "[equations][angular_momentum][unit]") {
|
||||
for (double target : {-1.0, std::numeric_limits<double>::infinity(), std::numeric_limits<double>::quiet_NaN()}) {
|
||||
CHECK_THROWS_AS(Equation{target}, std::invalid_argument);
|
||||
}
|
||||
CHECK_NOTHROW(Equation{0.0});
|
||||
CHECK_THROWS_AS((Equation{1.0, {0.0, 0.0, 0.0}}), std::invalid_argument);
|
||||
CHECK_THROWS_AS((Equation{1.0, {0.0, 0.0, 1.0}, {0.0, std::numeric_limits<double>::quiet_NaN(), 0.0}}), std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST_CASE("Angular momentum rejects overflow in integrated moments and residuals", "[equations][angular_momentum][unit]") {
|
||||
// A long cylinder-like box keeps point values finite while their volume integral overflows.
|
||||
auto mesh = make_mesh(::mfem::Element::HEXAHEDRON);
|
||||
for (int vertex = 0; vertex < mesh.GetNV(); ++vertex) { mesh.GetVertex(vertex)[2] *= 100.0; }
|
||||
GravityFixture f(std::move(mesh));
|
||||
auto values = zeros(f);
|
||||
const double maximum = std::numeric_limits<double>::max();
|
||||
for (const double target : {0.0, maximum}) {
|
||||
auto op = make_operator(f, target);
|
||||
values.density = 1.0;
|
||||
values.omega = target == 0.0 ? 0.1 * maximum : -0.01 * maximum;
|
||||
const auto rejected = op.try_prepare(state(f, values), values.displacement, f.workspace);
|
||||
REQUIRE_FALSE(rejected.has_value());
|
||||
CHECK(rejected.error().reason == backend::PreparationRejectionReason::NON_FINITE);
|
||||
CHECK_FALSE(op.is_prepared());
|
||||
}
|
||||
auto op = make_operator(f, 0.0);
|
||||
values.density = 0.1 * maximum;
|
||||
values.omega = 0.0;
|
||||
const auto rejected = op.try_prepare(state(f, values), values.displacement, f.workspace);
|
||||
REQUIRE_FALSE(rejected.has_value());
|
||||
CHECK(rejected.error().reason == backend::PreparationRejectionReason::EQUATION);
|
||||
CHECK_FALSE(op.is_prepared());
|
||||
values.density = 1.0;
|
||||
CHECK(residual(f, op, values) == 0.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Angular momentum handles zero speed signed speed and default quadrature", "[equations][angular_momentum][integration]") {
|
||||
for (const auto geometry : {::mfem::Element::TETRAHEDRON, ::mfem::Element::HEXAHEDRON}) {
|
||||
GravityFixture f(geometry);
|
||||
Values values{density(f, 0.31), constant(1, 0.0), displacement(f, 0.43)};
|
||||
Prepared op{Equation{0.41}, f.angular_velocity_binding, f.displacement_binding, f.compactification, f.mapper};
|
||||
CHECK(residual(f, op, values) == -0.41);
|
||||
CHECK(relative(moment(f, op), reference(f, values).moment) < 2.0e-13);
|
||||
const Values direction{density(f, 0.67, true), constant(1, 0.0), displacement(f, -0.79)};
|
||||
CHECK(action(f, op, direction) == 0.0);
|
||||
values.omega = -0.63;
|
||||
CHECK(relative(residual(f, op, values), -0.63 * reference(f, values).moment - 0.41) < 2.0e-13);
|
||||
values.density = 0.0;
|
||||
CHECK(residual(f, op, values) == -0.41);
|
||||
CHECK(moment(f, op) == 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Angular momentum preserves physical admissibility", "[equations][angular_momentum][unit]") {
|
||||
GravityFixture f;
|
||||
Values values{density(f, 0.31), constant(1, 0.63), displacement(f, 0.43)};
|
||||
VolumeEquationFixture op{Equation{0.41}, f.angular_velocity_binding, f.displacement_binding, f.compactification, f.mapper, reference_policy()};
|
||||
CHECK(relative(residual(f, op, values), 0.63 * reference(f, values).moment - 0.41) < 2.0e-13);
|
||||
const Values direction{density(f, 0.67, true), constant(1, -0.37), displacement(f, -0.79)};
|
||||
CHECK(relative(action(f, op, direction), reference(f, values, {0.0, 0.0, 1.0}, {}, &direction).action) < 2.0e-11);
|
||||
values.density = -0.91;
|
||||
const auto rejected = op.try_prepare(state(f, values), values.displacement, f.workspace);
|
||||
REQUIRE_FALSE(rejected.has_value());
|
||||
CHECK(rejected.error().reason == backend::PreparationRejectionReason::EQUATION);
|
||||
CHECK_FALSE(op.is_prepared());
|
||||
}
|
||||
|
||||
TEST_CASE("One angular speed drives all supported equilibrium rows consistently", "[equations][angular_momentum][integration]") {
|
||||
// Port the physical columns of MeanField stellar_equilibrium_system.cpp:253.
|
||||
// Root manifests, surface projection, and Lane-Emden seed generation are separate infrastructure.
|
||||
auto fixture = gravity_port::stellar_fixture();
|
||||
auto& f = *fixture;
|
||||
Values values{density(f, 0.31), constant(1, 0.63), displacement(f, 0.43)};
|
||||
auto direction = zeros(f);
|
||||
direction.omega = -0.37;
|
||||
const auto h = f.enthalpy(), phi = f.potential(), c = constant(1, 0.39);
|
||||
const auto zero_h = constant(h.Size(), 0.0), zero_phi = constant(phi.Size(), 0.0), zero_c = constant(1, 0.0);
|
||||
auto bind_all = [&](const Values& v, const Vector& enthalpy, const Vector& potential, const Vector& bernoulli) {
|
||||
return backend::make_term_coefficients(backend::bind(f.density_binding, v.density), backend::bind(f.angular_velocity_binding, v.omega),
|
||||
backend::bind(f.enthalpy_binding, enthalpy), backend::bind(f.potential_binding, potential),
|
||||
backend::bind(f.constant_binding, bernoulli));
|
||||
};
|
||||
const auto base = bind_all(values, h, phi, c), delta = bind_all(direction, zero_h, zero_phi, zero_c);
|
||||
auto make_row = [&](auto equation, const auto& binding) {
|
||||
return VolumeEquationFixture{equation, binding, f.displacement_binding, f.compactification, f.mapper, reference_policy()};
|
||||
};
|
||||
const Axis axis{0.0, 0.0, 3.0};
|
||||
const eos::models::Polytrope eos{1.0, 0.25};
|
||||
auto momentum = make_row(Equation{0.1, axis}, f.angular_velocity_binding);
|
||||
auto density_balance = make_row(serif::equations::BarotropicDensityBalance{eos, dimensions::PressureValue{0.0}, axis}, f.density_binding);
|
||||
auto mass = make_row(serif::equations::MassNormalization{1.0}, f.constant_binding);
|
||||
auto closure = make_row(::serif::tests::equations::BarotropicClosure{eos}, f.density_binding);
|
||||
auto poisson = make_row(serif::equations::PrimalPoissonBalance{four_pi_G}, f.potential_binding);
|
||||
|
||||
auto check_rotating_row = [&](auto& row, double tolerance, bool zero_at_rest) {
|
||||
for (double omega : {0.63, 0.0}) {
|
||||
values.omega = omega;
|
||||
row.prepare(base, values.displacement, f.workspace);
|
||||
Vector actual, plus, minus;
|
||||
row.apply_jacobian(delta, direction.displacement, f.workspace, actual);
|
||||
if (omega == 0.0 && zero_at_rest) { CHECK(actual.Norml2() == 0.0); }
|
||||
else { CHECK(actual.Norml2() > 0.0); }
|
||||
constexpr double step = 1.0e-5;
|
||||
values.omega = omega + step * direction.omega[0];
|
||||
row.prepare(base, values.displacement, f.workspace);
|
||||
row.residual(f.workspace, plus);
|
||||
values.omega = omega - step * direction.omega[0];
|
||||
row.prepare(base, values.displacement, f.workspace);
|
||||
row.residual(f.workspace, minus);
|
||||
plus -= minus;
|
||||
plus /= 2.0 * step;
|
||||
const double scale = std::max({1.0, actual.Norml2(), plus.Norml2()});
|
||||
plus -= actual;
|
||||
CHECK(plus.Norml2() / scale < tolerance);
|
||||
}
|
||||
};
|
||||
check_rotating_row(momentum, 3.0e-10, false);
|
||||
check_rotating_row(density_balance, 3.0e-7, true);
|
||||
values.omega = 0.63;
|
||||
auto check_independent_row = [&](auto& row) {
|
||||
row.prepare(base, values.displacement, f.workspace);
|
||||
Vector actual;
|
||||
row.apply_jacobian(delta, direction.displacement, f.workspace, actual);
|
||||
CHECK(actual.Norml2() == 0.0);
|
||||
};
|
||||
check_independent_row(mass);
|
||||
check_independent_row(closure);
|
||||
check_independent_row(poisson);
|
||||
}
|
||||
Reference in New Issue
Block a user