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);
|
||||
}
|
||||
105
tests/equations/barotropic_density.cpp
Normal file
105
tests/equations/barotropic_density.cpp
Normal file
@@ -0,0 +1,105 @@
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
|
||||
#include "serif/equations/barotropic_density.hpp"
|
||||
#include "serif/solver/normalization/scales.hpp"
|
||||
#include "serif/tests/equations/barotropic_eos.hpp"
|
||||
|
||||
namespace {
|
||||
namespace dimensions = serif::dimensions;
|
||||
namespace eos = serif::eos;
|
||||
|
||||
using MixedPowerBarotrope = serif::tests::equations::MixedPowerBarotrope;
|
||||
using Balance = serif::equations::BarotropicDensityBalance<MixedPowerBarotrope>;
|
||||
using Point = serif::equations::EquationPoint<Balance::Operands>;
|
||||
|
||||
/** @brief Check one scalar physical column independently of the other field and geometry columns. */
|
||||
template <typename Term>
|
||||
void check_field_derivative(const Balance& equation, const Point& point) {
|
||||
constexpr double step = 2.0e-6;
|
||||
Point positive = point, negative = point;
|
||||
positive.fields.get<Term>() += step;
|
||||
negative.fields.get<Term>() -= step;
|
||||
const double difference = (equation.evaluate(positive).residual - equation.evaluate(negative).residual) / (2.0 * step);
|
||||
CHECK_THAT(equation.evaluate(point).field_derivatives.get<Term>(), Catch::Matchers::WithinAbs(difference, 2.0e-8));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Barotropic density balance preserves finite pressure and primitive gauge invariance", "[equations][barotropic_density][unit]") {
|
||||
const Balance equation{MixedPowerBarotrope{}, dimensions::PressureValue{2.0}};
|
||||
const Balance shifted{MixedPowerBarotrope{.gauge = 7.5}, dimensions::PressureValue{2.0}};
|
||||
Point point;
|
||||
point.fields.get<Balance::Density>() = 2.0;
|
||||
point.fields.get<Balance::Potential>() = -12.0;
|
||||
point.fields.get<Balance::SurfaceLevel>() = -2.0 / 3.0;
|
||||
const auto balance = equation.evaluate(point);
|
||||
const auto shifted_balance = shifted.evaluate(point);
|
||||
CHECK_THAT(balance.residual, Catch::Matchers::WithinAbs(0.0, 3.0e-15));
|
||||
CHECK_THAT(shifted_balance.residual, Catch::Matchers::WithinAbs(balance.residual, 4.0e-15));
|
||||
CHECK(shifted_balance.field_derivatives.values == balance.field_derivatives.values);
|
||||
CHECK(shifted_balance.position_derivative == balance.position_derivative);
|
||||
CHECK(equation.surface_pressure().value() == 2.0);
|
||||
|
||||
const Balance vacuum{MixedPowerBarotrope{}};
|
||||
CHECK_THAT(vacuum.evaluate(point).residual, Catch::Matchers::WithinAbs(10.0 / 3.0, 3.0e-15));
|
||||
point.fields.get<Balance::Density>() = 1.0;
|
||||
point.fields.get<Balance::Potential>() = -2.0;
|
||||
point.fields.get<Balance::SurfaceLevel>() = -2.0;
|
||||
CHECK(equation.evaluate(point).residual == 0.0);
|
||||
CHECK(shifted.evaluate(point).residual == 0.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Barotropic density tangent includes every field and physical position", "[equations][barotropic_density][unit]") {
|
||||
const Balance equation{MixedPowerBarotrope{}, dimensions::PressureValue{2.0}, {1.0, 2.0, 3.0}, {0.2, -0.1, 0.3}};
|
||||
Point point;
|
||||
point.fields.get<Balance::Density>() = 1.3;
|
||||
point.fields.get<Balance::Potential>() = -3.2;
|
||||
point.fields.get<Balance::SurfaceLevel>() = -0.9;
|
||||
point.fields.get<Balance::AngularVelocity>() = 0.8;
|
||||
point.position = {1.2, 0.7, -0.2};
|
||||
// The displacement from the rotation origin is (1, 0.8, -0.5), with projection 1.1/sqrt(14).
|
||||
const double centrifugal_potential = 0.5 * (1.0 + 0.64 + 0.25 - 1.21 / 14.0);
|
||||
const double expected = 2.6 + (4.0 / 3.0) * 1.3 * 1.3 * 1.3 - 10.0 / 3.0 - 3.2 - 0.64 * centrifugal_potential + 0.9;
|
||||
const auto balance = equation.evaluate(point);
|
||||
CHECK_THAT(balance.residual, Catch::Matchers::WithinAbs(expected, 4.0e-15));
|
||||
CHECK_THAT(balance.field_derivatives.get<Balance::Density>(), Catch::Matchers::WithinAbs(2.0 + 4.0 * 1.3 * 1.3, 2.0e-15));
|
||||
check_field_derivative<Balance::Density>(equation, point);
|
||||
check_field_derivative<Balance::Potential>(equation, point);
|
||||
check_field_derivative<Balance::SurfaceLevel>(equation, point);
|
||||
check_field_derivative<Balance::AngularVelocity>(equation, point);
|
||||
constexpr double step = 2.0e-6;
|
||||
for (int axis = 0; axis < 3; ++axis) {
|
||||
Point positive = point, negative = point;
|
||||
positive.position[axis] += step;
|
||||
negative.position[axis] -= step;
|
||||
const double difference = (equation.evaluate(positive).residual - equation.evaluate(negative).residual) / (2.0 * step);
|
||||
CHECK_THAT(balance.position_derivative[axis], Catch::Matchers::WithinAbs(difference, 2.0e-8));
|
||||
}
|
||||
point.fields.get<Balance::AngularVelocity>() = 0.0;
|
||||
CHECK(equation.evaluate(point).position_derivative == serif::discretization::forms::VectorValue{});
|
||||
}
|
||||
|
||||
TEST_CASE("Barotropic density balance rejects invalid interior states without clipping", "[equations][barotropic_density][unit]") {
|
||||
const Balance equation{MixedPowerBarotrope{}};
|
||||
Point point;
|
||||
for (const double density : {0.0, -0.1, std::numeric_limits<double>::infinity(), std::numeric_limits<double>::quiet_NaN()}) {
|
||||
point.fields.get<Balance::Density>() = density;
|
||||
CHECK_THROWS_AS(equation.evaluate(point), eos::EOSEvaluationError);
|
||||
}
|
||||
point.fields.get<Balance::Density>() = 1.0e200;
|
||||
CHECK_THROWS_AS(equation.evaluate(point), eos::EOSEvaluationError);
|
||||
for (const double pressure : {-0.1, std::numeric_limits<double>::infinity(), std::numeric_limits<double>::quiet_NaN()}) {
|
||||
CHECK_THROWS_AS((Balance{MixedPowerBarotrope{}, dimensions::PressureValue{pressure}}), eos::EOSEvaluationError);
|
||||
}
|
||||
CHECK_THROWS_AS((Balance{MixedPowerBarotrope{.gauge = std::numeric_limits<double>::infinity()}}), eos::EOSEvaluationError);
|
||||
}
|
||||
|
||||
TEST_CASE("Pressure potential uses the gravitational specific energy reference scale", "[equations][barotropic_density][unit]") {
|
||||
const auto scales = serif::solver::normalization::prepare_stellar_reference_scales(dimensions::MassValue{2.0},
|
||||
dimensions::LengthValue{4.0}, dimensions::QuantityValue<dimensions::GravitationalConstant>{3.0});
|
||||
REQUIRE(scales.has_value());
|
||||
CHECK(serif::solver::normalization::characteristic_scale(*scales, dimensions::BarotropicPressurePotential{}).value() == 1.5);
|
||||
}
|
||||
143
tests/equations/barotropic_density_assembly.cpp
Normal file
143
tests/equations/barotropic_density_assembly.cpp
Normal file
@@ -0,0 +1,143 @@
|
||||
#include <cmath>
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "serif/equations/barotropic_density.hpp"
|
||||
#include "serif/tests/equations/barotropic_eos.hpp"
|
||||
#include "serif/tests/equations/gravity_fixture.hpp"
|
||||
|
||||
namespace {
|
||||
using Balance = serif::equations::BarotropicDensityBalance<serif::tests::equations::MixedPowerBarotrope>;
|
||||
}
|
||||
|
||||
TEST_CASE("Barotropic density assembles a nonpolytropic value balance and complete ALE tangent", "[equations][barotropic_density][unit]") {
|
||||
using namespace serif::tests::equations;
|
||||
|
||||
for (const auto geometry : {::mfem::Element::HEXAHEDRON, ::mfem::Element::TETRAHEDRON}) {
|
||||
gravity::GravityFixture fixture(geometry);
|
||||
auto density = fixture.density(), potential = fixture.potential();
|
||||
auto surface_level = constant(1, -0.8), omega = constant(1, 0.2), displacement = fixture.displacement(0.3);
|
||||
/** @brief Bind exactly the four physical inputs; the EOS supplies every thermodynamic value. */
|
||||
const auto fields = [&](const auto& rho, const auto& phi, const auto& level, const auto& rotation) {
|
||||
return backend::make_term_coefficients(backend::bind(fixture.density_binding, rho),
|
||||
backend::bind(fixture.potential_binding, phi), backend::bind(fixture.constant_binding, level),
|
||||
backend::bind(fixture.angular_velocity_binding, rotation));
|
||||
};
|
||||
const Balance balance{MixedPowerBarotrope{.gauge = 7.5}, serif::dimensions::PressureValue{0.3125},
|
||||
{1.0, 2.0, 3.0}, {0.2, -0.1, 0.3}};
|
||||
VolumeEquationFixture equation{balance, fixture.density_binding, fixture.displacement_binding,
|
||||
fixture.compactification, fixture.mapper, gravity::reference_policy()};
|
||||
equation.prepare(fields(density, potential, surface_level, omega), constant(displacement.Size(), 0.0), fixture.workspace);
|
||||
::mfem::Vector residual;
|
||||
equation.residual(fixture.workspace, residual);
|
||||
|
||||
/** @brief Integrate the analytic physical residual independently of the equation's evaluator. */
|
||||
::mfem::FunctionCoefficient expected_balance([](const ::mfem::Vector& position) {
|
||||
const double rho = 0.55 + 0.025 * position[0] - 0.010 * position[1] + 0.006 * position[2];
|
||||
const double phi = -0.5 + 0.1 * position[0] * position[1] - 0.07 * position[2] * position[2];
|
||||
const double x = position[0] - 0.2, y = position[1] + 0.1, z = position[2] - 0.3;
|
||||
const double axial_projection = x + 2.0 * y + 3.0 * z;
|
||||
const double centrifugal = 0.5 * (x * x + y * y + z * z - axial_projection * axial_projection / 14.0);
|
||||
// P_surface = 5/16 fixes rho_surface = 1/2 and A_surface - gauge = 7/6.
|
||||
return 2.0 * rho + (4.0 / 3.0) * rho * rho * rho - 7.0 / 6.0 + phi - 0.04 * centrifugal + 0.8;
|
||||
});
|
||||
::mfem::LinearForm reference(&fixture.density_space);
|
||||
::mfem::Array<int> stellar{1, 1, 0};
|
||||
const auto& rule = ::mfem::IntRules.Get(fixture.mesh.GetElementBaseGeometry(0), gravity::reference_order);
|
||||
auto* integrator = new ::mfem::DomainLFIntegrator(expected_balance);
|
||||
integrator->SetIntRule(&rule);
|
||||
reference.AddDomainIntegrator(integrator, stellar);
|
||||
reference.Assemble();
|
||||
CHECK(relative_error(residual, fixture.density_binding.gather(reference)) < 3.0e-12);
|
||||
|
||||
const auto density_direction = deterministic(density.Size(), 0.1), potential_direction = deterministic(potential.Size(), 0.3);
|
||||
const auto level_direction = constant(1, -0.05), omega_direction = constant(1, 0.07), displacement_direction = fixture.displacement(0.2);
|
||||
equation.prepare(fields(density, potential, surface_level, omega), displacement, fixture.workspace);
|
||||
::mfem::Vector action, transpose;
|
||||
equation.apply_jacobian(fields(density_direction, potential_direction, level_direction, omega_direction),
|
||||
displacement_direction, fixture.workspace, action);
|
||||
const auto dual = deterministic(residual.Size(), 0.7);
|
||||
equation.apply_jacobian_transpose(dual, fixture.workspace, transpose);
|
||||
const auto direction = packed(packed(density_direction, potential_direction, level_direction), omega_direction, displacement_direction);
|
||||
CHECK(std::abs(dual * action - direction * transpose) < 4.0e-11);
|
||||
|
||||
constexpr double step = 1.0e-6;
|
||||
auto plus_density = density, minus_density = density, plus_potential = potential, minus_potential = potential;
|
||||
auto plus_level = surface_level, minus_level = surface_level, plus_omega = omega, minus_omega = omega;
|
||||
auto plus_displacement = displacement, minus_displacement = displacement;
|
||||
plus_density.Add(step, density_direction);
|
||||
minus_density.Add(-step, density_direction);
|
||||
plus_potential.Add(step, potential_direction);
|
||||
minus_potential.Add(-step, potential_direction);
|
||||
plus_level.Add(step, level_direction);
|
||||
minus_level.Add(-step, level_direction);
|
||||
plus_omega.Add(step, omega_direction);
|
||||
minus_omega.Add(-step, omega_direction);
|
||||
plus_displacement.Add(step, displacement_direction);
|
||||
minus_displacement.Add(-step, displacement_direction);
|
||||
::mfem::Vector positive, negative;
|
||||
equation.prepare(fields(plus_density, plus_potential, plus_level, plus_omega), plus_displacement, fixture.workspace);
|
||||
equation.residual(fixture.workspace, positive);
|
||||
equation.prepare(fields(minus_density, minus_potential, minus_level, minus_omega), minus_displacement, fixture.workspace);
|
||||
equation.residual(fixture.workspace, negative);
|
||||
positive -= negative;
|
||||
positive /= 2.0 * step;
|
||||
CHECK(relative_error(action, positive) < 4.0e-8);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Reconstructed density retains the field and geometry Jacobian including its transpose", "[equations][unit][reconstruction]") {
|
||||
using namespace serif::tests::equations;
|
||||
namespace reconstruction = serif::discretization::reconstruction;
|
||||
namespace discretization_backend = serif::discretization::backend::mfem;
|
||||
using Density = blocks::density::mass;
|
||||
using PolytropicBalance = serif::equations::BarotropicDensityBalance<serif::eos::models::Polytrope>;
|
||||
|
||||
gravity::GravityFixture fixture(::mfem::Mesh::MakeCartesian3D(2, 1, 1, ::mfem::Element::HEXAHEDRON));
|
||||
const double exponent = 5.0 / 3.0;
|
||||
const auto reconstruction_plan = discretization_backend::make_reconstruction(
|
||||
reconstruction::SurfacePower<Density>{.density_exponent = exponent}, fixture.density_binding);
|
||||
discretization_backend::TermBinding<Density, Schema> density_binding(fixture.density_space, reconstruction_plan);
|
||||
auto density = fixture.project(density_binding, [exponent](const ::mfem::Vector& point) {
|
||||
return std::pow(point[2], exponent) * (1.0 + 0.1 * point[0]);
|
||||
});
|
||||
const auto density_direction = fixture.project(density_binding, [exponent](const ::mfem::Vector& point) {
|
||||
return 0.1 * std::pow(point[2], exponent) * (point[0] - 0.3 * point[1]);
|
||||
});
|
||||
const auto potential = fixture.potential(), level = constant(1, -0.8), omega = constant(1, 0.2);
|
||||
const auto displacement = fixture.displacement(0.3), displacement_direction = fixture.displacement(0.2);
|
||||
const auto fields = [&](const auto& rho) {
|
||||
return backend::make_term_coefficients(backend::bind(density_binding, rho),
|
||||
backend::bind(fixture.potential_binding, potential), backend::bind(fixture.constant_binding, level),
|
||||
backend::bind(fixture.angular_velocity_binding, omega));
|
||||
};
|
||||
VolumeEquationFixture equation{PolytropicBalance{serif::eos::models::Polytrope{exponent, 0.7}, serif::dimensions::PressureValue{0.0}},
|
||||
density_binding, fixture.displacement_binding, fixture.compactification, fixture.mapper, gravity::reference_policy()};
|
||||
equation.prepare(fields(density), displacement, fixture.workspace);
|
||||
CHECK(equation.integration_order(0) == ::mfem::IntRules.Get(::mfem::Geometry::CUBE, gravity::reference_order).GetOrder());
|
||||
::mfem::Vector action, transpose;
|
||||
const auto potential_direction = constant(potential.Size(), 0.0), level_direction = constant(1, 0.0), omega_direction = constant(1, 0.0);
|
||||
equation.apply_jacobian(backend::make_term_coefficients(backend::bind(density_binding, density_direction),
|
||||
backend::bind(fixture.potential_binding, potential_direction), backend::bind(fixture.constant_binding, level_direction),
|
||||
backend::bind(fixture.angular_velocity_binding, omega_direction)), displacement_direction, fixture.workspace, action);
|
||||
const auto dual = deterministic(density_binding.size(), 0.7);
|
||||
equation.apply_jacobian_transpose(dual, fixture.workspace, transpose);
|
||||
const auto direction = packed(packed(density_direction, potential_direction, level_direction), omega_direction, displacement_direction);
|
||||
CHECK(std::abs(dual * action - direction * transpose) < 4e-11);
|
||||
for (const double step : {1e-5, 1e-6}) {
|
||||
auto plus_density = density, minus_density = density;
|
||||
auto plus_displacement = displacement, minus_displacement = displacement;
|
||||
plus_density.Add(step, density_direction);
|
||||
minus_density.Add(-step, density_direction);
|
||||
plus_displacement.Add(step, displacement_direction);
|
||||
minus_displacement.Add(-step, displacement_direction);
|
||||
::mfem::Vector plus, minus;
|
||||
equation.prepare(fields(plus_density), plus_displacement, fixture.workspace);
|
||||
equation.residual(fixture.workspace, plus);
|
||||
equation.prepare(fields(minus_density), minus_displacement, fixture.workspace);
|
||||
equation.residual(fixture.workspace, minus);
|
||||
plus -= minus;
|
||||
plus /= 2.0 * step;
|
||||
CHECK(relative_error(action, plus) < 4e-8);
|
||||
}
|
||||
}
|
||||
79
tests/equations/barycenter.cpp
Normal file
79
tests/equations/barycenter.cpp
Normal file
@@ -0,0 +1,79 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include "serif/solver/normalization/scales.hpp"
|
||||
#include "serif/solver/normalization/physical_coordinates.hpp"
|
||||
#include "serif/equations/barycenter.hpp"
|
||||
#include "serif/utils/serialize/equations.hpp"
|
||||
|
||||
namespace {
|
||||
namespace equations = serif::equations;
|
||||
namespace dimensions = serif::dimensions;
|
||||
using Density = serif::discretization::blocks::density::mass;
|
||||
|
||||
/** @brief Check the density partial and every explicit position partial by central differences. */
|
||||
template <typename Equation, typename Point>
|
||||
void check_partials(const Equation& equation, const Point& point) {
|
||||
const auto linearization = equation.evaluate(point);
|
||||
for (const double step : {1.0e-5, 1.0e-6}) {
|
||||
auto plus = point, minus = point;
|
||||
plus.fields.template get<Density>() += step;
|
||||
minus.fields.template get<Density>() -= step;
|
||||
const double difference = (equation.evaluate(plus).residual - equation.evaluate(minus).residual) / (2.0 * step);
|
||||
const double expected = linearization.field_derivatives.template get<Density>();
|
||||
CHECK(std::abs(difference - expected) < 2.0e-9 * std::max(1.0, std::abs(expected)));
|
||||
}
|
||||
for (int axis = 0; axis < 3; ++axis) {
|
||||
auto plus = point, minus = point;
|
||||
constexpr double step = 1.0e-5;
|
||||
plus.position[axis] += step;
|
||||
minus.position[axis] -= step;
|
||||
const double difference = (equation.evaluate(plus).residual - equation.evaluate(minus).residual) / (2.0 * step);
|
||||
CHECK(std::abs(difference - linearization.position_derivative[axis]) < 2.0e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Barycenter constraints differentiate all axes about a displaced center and persist", "[equations][barycenter][unit]") {
|
||||
const auto check_axis = []<int Axis>() {
|
||||
using Center = equations::BarycenterConstraint<Axis>;
|
||||
const std::array<double, 3> origin{0.1, -0.2, 0.3};
|
||||
const std::array<double, 3> position{0.6, 0.3, -0.4};
|
||||
constexpr double density = 1.2;
|
||||
constexpr double mass = 2.7;
|
||||
equations::EquationPoint<typename Center::Operands> center;
|
||||
center.position = position;
|
||||
center.fields.template get<Density>() = density;
|
||||
const Center center_law{mass, origin};
|
||||
CHECK(center_law.evaluate(center).residual == Catch::Approx(density * (position[Axis] - origin[Axis]) / mass));
|
||||
check_partials(center_law, center);
|
||||
namespace serialization = serif::utils::serialize;
|
||||
const auto saved = serialization::encode(center_law);
|
||||
const auto restored = serialization::decode<Center>(saved);
|
||||
REQUIRE(restored);
|
||||
CHECK(serialization::encode(*restored) == saved);
|
||||
for (const double invalid : {std::numeric_limits<double>::infinity(), std::numeric_limits<double>::quiet_NaN()}) {
|
||||
auto bad_center = origin;
|
||||
bad_center[Axis] = invalid;
|
||||
CHECK_THROWS(Center(mass, bad_center));
|
||||
}
|
||||
for (const double invalid : {0.0, -0.1, std::numeric_limits<double>::infinity(), std::numeric_limits<double>::quiet_NaN()}) {
|
||||
CHECK_THROWS(Center{invalid});
|
||||
}
|
||||
};
|
||||
check_axis.template operator()<0>();
|
||||
check_axis.template operator()<1>();
|
||||
check_axis.template operator()<2>();
|
||||
}
|
||||
|
||||
TEST_CASE("Frame acceleration coordinates use the stellar acceleration scale", "[equations][barycenter][unit]") {
|
||||
namespace normalization = serif::solver::normalization;
|
||||
const auto scales = normalization::prepare_stellar_reference_scales(dimensions::MassValue{12.0}, dimensions::LengthValue{3.0},
|
||||
dimensions::QuantityValue<dimensions::GravitationalConstant>{2.0});
|
||||
REQUIRE(scales);
|
||||
CHECK(normalization::characteristic_scale(*scales, dimensions::Acceleration{}).value() == Catch::Approx(24.0 / 9.0));
|
||||
}
|
||||
91
tests/equations/basis_lifetime_meanfield.cpp
Normal file
91
tests/equations/basis_lifetime_meanfield.cpp
Normal file
@@ -0,0 +1,91 @@
|
||||
#include <cmath>
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "mfem/fem/fe_coll.hpp"
|
||||
#include "serif/backend/mfem/discretization/basis_evaluation.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace serif::discretization::backend::mfem;
|
||||
|
||||
/// Require repeatability after the original finite element, quadrature, and cache have been destroyed.
|
||||
void check_retained_basis(const int basis)
|
||||
{
|
||||
BasisEvaluation retained;
|
||||
BasisWorkspace workspace;
|
||||
::mfem::Vector coefficients, dual, expected_values, expected_transpose;
|
||||
{
|
||||
const ::mfem::H1_FECollection collection(3, 3, basis);
|
||||
const ::mfem::FiniteElement& element = *collection.FiniteElementForGeometry(::mfem::Geometry::CUBE);
|
||||
const ::mfem::IntegrationRule rule(::mfem::IntRules.Get(::mfem::Geometry::CUBE, 7));
|
||||
TensorBasisCache cache;
|
||||
retained.sample(element, rule, ReferenceOperation::value_and_gradient, 1, cache, ContractionMode::automatic);
|
||||
CHECK(retained.uses_tensor() == (basis != ::mfem::BasisType::Positive));
|
||||
coefficients.SetSize(retained.width());
|
||||
dual.SetSize(retained.height());
|
||||
for (int i = 0; i < coefficients.Size(); ++i)
|
||||
{
|
||||
coefficients[i] = std::sin(0.37 * (i + 1));
|
||||
}
|
||||
for (int i = 0; i < dual.Size(); ++i)
|
||||
{
|
||||
dual[i] = std::cos(0.23 * (i + 1));
|
||||
}
|
||||
retained.evaluate(coefficients, expected_values, workspace);
|
||||
retained.integrate(dual, expected_transpose, workspace);
|
||||
}
|
||||
|
||||
::mfem::Vector values, transpose;
|
||||
retained.evaluate(coefficients, values, workspace);
|
||||
retained.integrate(dual, transpose, workspace);
|
||||
values -= expected_values;
|
||||
transpose -= expected_transpose;
|
||||
CHECK(values.Normlinf() == 0.0);
|
||||
CHECK(transpose.Normlinf() == 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("MeanField scalar basis actions survive cache finite element and rule destruction", "[equations][tensor_basis][unit]")
|
||||
{
|
||||
check_retained_basis(::mfem::BasisType::GaussLobatto);
|
||||
check_retained_basis(::mfem::BasisType::Positive);
|
||||
}
|
||||
|
||||
TEST_CASE("MeanField basis cache follows rule contents without mutating published factors", "[equations][tensor_basis][unit]")
|
||||
{
|
||||
::mfem::H1_FECollection collection(3, 3);
|
||||
const ::mfem::FiniteElement& element = *collection.FiniteElementForGeometry(::mfem::Geometry::CUBE);
|
||||
const ::mfem::IntegrationRule& rule = ::mfem::IntRules.Get(::mfem::Geometry::CUBE, 7);
|
||||
::mfem::IntegrationRule copy(rule), changed_weight(rule), moved_point(rule);
|
||||
changed_weight.IntPoint(0).weight *= 1.25;
|
||||
moved_point.IntPoint(0).x += 0.03125;
|
||||
TensorBasisCache cache;
|
||||
const auto original = cache.get(element, rule, ReferenceOperation::value_and_gradient, 1);
|
||||
REQUIRE(original);
|
||||
::mfem::Vector coefficients(original->dofs()), expected, values;
|
||||
coefficients = 0.37;
|
||||
TensorWorkspace workspace;
|
||||
original->apply(coefficients, expected, workspace);
|
||||
|
||||
CHECK(cache.get(element, copy, ReferenceOperation::value_and_gradient, 1) == original);
|
||||
const auto reweighted = cache.get(element, changed_weight, ReferenceOperation::value_and_gradient, 1);
|
||||
REQUIRE(reweighted);
|
||||
CHECK(reweighted != original);
|
||||
CHECK_FALSE(cache.get(element, moved_point, ReferenceOperation::value_and_gradient, 1));
|
||||
CHECK(cache.size() == 3);
|
||||
copy.IntPoint(0).x = moved_point.IntPoint(0).x;
|
||||
CHECK_FALSE(cache.get(element, copy, ReferenceOperation::value_and_gradient, 1));
|
||||
CHECK(cache.size() == 3);
|
||||
CHECK(cache.get(element, rule, ReferenceOperation::value_and_gradient, 1) == original);
|
||||
|
||||
// Weights identify cache entries, but basis evaluation introduces no integration weight.
|
||||
reweighted->apply(coefficients, values, workspace);
|
||||
values -= expected;
|
||||
CHECK(values.Normlinf() == 0.0);
|
||||
cache.clear();
|
||||
CHECK(cache.size() == 0);
|
||||
original->apply(coefficients, values, workspace);
|
||||
values -= expected;
|
||||
CHECK(values.Normlinf() == 0.0);
|
||||
}
|
||||
121
tests/equations/closure_conditions.cpp
Normal file
121
tests/equations/closure_conditions.cpp
Normal file
@@ -0,0 +1,121 @@
|
||||
// EOS/geometry conditions and tolerances from MeanField's prepared closure tests.
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "serif/tests/equations/fixture.hpp"
|
||||
#include "serif/tests/test_tags.hpp"
|
||||
|
||||
namespace serif::tests::equations {
|
||||
struct ClosureCondition final {
|
||||
double n, K, enthalpy_offset, enthalpy_gradient, density_factor, density_offset, density_gradient;
|
||||
double deformation, direction_phase;
|
||||
};
|
||||
|
||||
constexpr std::array<ClosureCondition, 3> closure_conditions{
|
||||
{{1.0, 0.8, 0.65, 0.06, 0.80, 0.015, 0.004, 0.0, 0.31}, {1.5, 1.2, 0.90, 0.09, 1.15, -0.003, 0.003, 0.45, 0.53},
|
||||
{3.0, 1.5, 1.20, 0.12, 1.40, 0.006, 0.002, 1.0, 0.79}}
|
||||
};
|
||||
|
||||
TEST_CASE("Equation Closure Covers Multiple EOS And Geometry Conditions", tags::integration & tags::equations) {
|
||||
Fixture f;
|
||||
for (const auto& condition : closure_conditions) {
|
||||
CAPTURE(condition.n, condition.K, condition.deformation);
|
||||
const eos::models::Polytrope eos(condition.n, condition.K);
|
||||
const auto enthalpy_function = [&](const ::mfem::Vector& x) {
|
||||
return condition.enthalpy_offset +
|
||||
condition.enthalpy_gradient * (0.50 * x[0] - 0.30 * x[1] + 0.20 * x[2]);
|
||||
};
|
||||
const auto h = f.project(f.enthalpy_binding, enthalpy_function);
|
||||
const auto rho = f.project(f.density_binding, [&](const ::mfem::Vector& x) {
|
||||
return condition.density_factor * std::pow(enthalpy_function(x) / eos.enthalpy_scale(), condition.n) +
|
||||
condition.density_offset +
|
||||
condition.density_gradient * (0.40 * x[0] + 0.25 * x[1] - 0.15 * x[2]);
|
||||
});
|
||||
const auto u = f.displacement(condition.deformation);
|
||||
auto drho = deterministic(rho.Size(), condition.direction_phase);
|
||||
auto dh = f.project(f.enthalpy_binding, [](const ::mfem::Vector& x) {
|
||||
return 0.07 + 0.015 * x[0] + 0.008 * x[2];
|
||||
});
|
||||
auto du = f.displacement(0.63);
|
||||
auto density_action = manual_closure(f, eos, rho, h, u, &drho);
|
||||
auto enthalpy_action = manual_closure(f, eos, rho, h, u, nullptr, &dh);
|
||||
auto geometry_action = manual_closure(f, eos, rho, h, u, nullptr, nullptr, &du);
|
||||
const double target =
|
||||
std::min({density_action.Norml2(), enthalpy_action.Norml2(), geometry_action.Norml2()});
|
||||
REQUIRE(target > 1.0e-12);
|
||||
drho *= target / density_action.Norml2();
|
||||
dh *= target / enthalpy_action.Norml2();
|
||||
du *= target / geometry_action.Norml2();
|
||||
PreparedClosure prepared(Closure{eos}, f.density_binding, f.displacement_binding, f.compactification, f.mapper);
|
||||
prepared.prepare(f.state(rho, h), u, f.workspace);
|
||||
::mfem::Vector residual, action;
|
||||
prepared.residual(f.workspace, residual);
|
||||
prepared.apply_jacobian(f.state(drho, dh), du, f.workspace, action);
|
||||
CHECK(relative_error(residual, manual_closure(f, eos, rho, h, u)) < 5.0e-12);
|
||||
CHECK(relative_error(action, manual_closure(f, eos, rho, h, u, &drho, &dh, &du)) < 5.0e-12);
|
||||
const int expected_order = std::max(4, 5 + static_cast<int>(std::ceil((condition.n - 1.0) * 3))) + 6;
|
||||
CHECK(prepared.integration_order(0) >= expected_order);
|
||||
constexpr double step = 1.0e-5;
|
||||
auto plus_rho = rho, minus_rho = rho, plus_h = h, minus_h = h, plus_u = u, minus_u = u;
|
||||
plus_rho.Add(step, drho);
|
||||
minus_rho.Add(-step, drho);
|
||||
plus_h.Add(step, dh);
|
||||
minus_h.Add(-step, dh);
|
||||
plus_u.Add(step, du);
|
||||
minus_u.Add(-step, du);
|
||||
auto difference = manual_closure(f, eos, plus_rho, plus_h, plus_u);
|
||||
difference -= manual_closure(f, eos, minus_rho, minus_h, minus_u);
|
||||
difference /= 2.0 * step;
|
||||
difference -= action;
|
||||
CHECK(difference.Norml2() / (3.0 * target) < 2.0e-7);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Exact Equation Closure Remains Zero Under Deformation", tags::integration & tags::equations) {
|
||||
Fixture f;
|
||||
const auto h = constant(f.enthalpy_binding.size(), 1.20);
|
||||
const auto rho = constant(f.density_binding.size(), std::pow(1.20 / 6.0, 3.0));
|
||||
auto reference_rho = rho;
|
||||
reference_rho += 1.0;
|
||||
const auto zero_rho = constant(rho.Size(), 0.0), zero_h = constant(h.Size(), 0.0), du = f.displacement(0.67);
|
||||
for (const double deformation : {0.0, 0.5, 1.0}) {
|
||||
CAPTURE(deformation);
|
||||
const auto u = f.displacement(deformation);
|
||||
PreparedClosure prepared(
|
||||
Closure{eos::models::Polytrope{3.0, 1.5}}, f.density_binding, f.displacement_binding, f.compactification,
|
||||
f.mapper
|
||||
);
|
||||
::mfem::Vector exact, exact_geometry, reference, reference_geometry;
|
||||
prepared.prepare(f.state(rho, h), u, f.workspace);
|
||||
prepared.residual(f.workspace, exact);
|
||||
prepared.apply_jacobian(f.state(zero_rho, zero_h), du, f.workspace, exact_geometry);
|
||||
prepared.prepare(f.state(reference_rho, h), u, f.workspace);
|
||||
prepared.residual(f.workspace, reference);
|
||||
prepared.apply_jacobian(f.state(zero_rho, zero_h), du, f.workspace, reference_geometry);
|
||||
REQUIRE(reference.Norml2() > 1.0e-12);
|
||||
REQUIRE(reference_geometry.Norml2() > 1.0e-14);
|
||||
CHECK(exact.Norml2() <= 5.0e-12 * reference.Norml2());
|
||||
CHECK(exact_geometry.Norml2() <= 5.0e-12 * reference_geometry.Norml2());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Equation Quadrature Resolves Both Closure Forms Through The Existing Policy", tags::unit & tags::equations) {
|
||||
Fixture f;
|
||||
const auto rho = f.density(), h = f.enthalpy(), u = f.displacement(0.0);
|
||||
using Mass = discretization::forms::mass<blocks::density::mass, blocks::density::mass>;
|
||||
using Source = discretization::forms::source<::serif::tests::equations::Enthalpy, blocks::density::mass>;
|
||||
discretization::quadrature::RuleSet rules;
|
||||
rules.set_form_control<Mass>({.fixed_order = 18});
|
||||
rules.set_form_control<Source>({.fixed_order = 8});
|
||||
PreparedClosure prepared(
|
||||
Closure{eos::models::Polytrope{1.0, 0.8}}, f.density_binding, f.displacement_binding, f.compactification,
|
||||
f.mapper, discretization::quadrature::Policy{rules}
|
||||
);
|
||||
prepared.prepare(f.state(rho, h), u, f.workspace);
|
||||
CHECK(prepared.integration_order(0) == ::mfem::IntRules.Get(::mfem::Geometry::CUBE, 18).GetOrder());
|
||||
CHECK_THROWS_AS(prepared.integration_order(2), std::out_of_range);
|
||||
}
|
||||
} // namespace serif::tests::equations
|
||||
36
tests/equations/closure_dilation.cpp
Normal file
36
tests/equations/closure_dilation.cpp
Normal file
@@ -0,0 +1,36 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "serif/tests/equations/fixture.hpp"
|
||||
#include "serif/tests/test_tags.hpp"
|
||||
|
||||
namespace serif::tests::equations {
|
||||
TEST_CASE("Equation Closure Geometry Has The Analytic Dilation Factor", tags::unit & tags::equations) {
|
||||
Fixture f;
|
||||
const auto rho = constant(f.density_binding.size(), 0.55), h = constant(f.enthalpy_binding.size(), 0.9);
|
||||
const auto zero_rho = constant(rho.Size(), 0.0), zero_h = constant(h.Size(), 0.0), zero_u = f.displacement(0.0);
|
||||
constexpr double dilation = 0.2, direction_scale = 0.07;
|
||||
::mfem::VectorFunctionCoefficient coefficient(3, [](const ::mfem::Vector& x, ::mfem::Vector& u) {
|
||||
u = x;
|
||||
u *= dilation;
|
||||
});
|
||||
::mfem::GridFunction displacement(&f.displacement_space);
|
||||
displacement.ProjectCoefficient(coefficient);
|
||||
const auto u = f.displacement_binding.gather(displacement);
|
||||
auto du = u;
|
||||
du *= direction_scale / dilation;
|
||||
PreparedClosure prepared(
|
||||
Closure{eos::models::Polytrope{3.0, 1.5}}, f.density_binding, f.displacement_binding, f.compactification,
|
||||
f.mapper
|
||||
);
|
||||
::mfem::Vector identity, deformed, action;
|
||||
prepared.prepare(f.state(rho, h), zero_u, f.workspace);
|
||||
prepared.residual(f.workspace, identity);
|
||||
prepared.prepare(f.state(rho, h), u, f.workspace);
|
||||
prepared.residual(f.workspace, deformed);
|
||||
prepared.apply_jacobian(f.state(zero_rho, zero_h), du, f.workspace, action);
|
||||
identity *= std::pow(1.0 + dilation, 3.0);
|
||||
CHECK(relative_error(deformed, identity) < 5.0e-12);
|
||||
deformed *= 3.0 * direction_scale / (1.0 + dilation);
|
||||
CHECK(relative_error(action, deformed) < 5.0e-12);
|
||||
}
|
||||
} // namespace serif::tests::equations
|
||||
175
tests/equations/closure_kernels.cpp
Normal file
175
tests/equations/closure_kernels.cpp
Normal file
@@ -0,0 +1,175 @@
|
||||
// Numerical contracts ported from MeanField tests/operators/kernels/barotropic_closure_kernels.cpp.
|
||||
|
||||
#include <memory>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "serif/tests/equations/fixture.hpp"
|
||||
#include "serif/tests/test_tags.hpp"
|
||||
|
||||
namespace serif::tests::equations {
|
||||
TEST_CASE("Equation Closure Vanishes For A Representable Constant State", tags::unit & tags::equations) {
|
||||
Fixture f;
|
||||
const eos::models::Polytrope eos(3.0, 1.5);
|
||||
const double h_value = 0.8;
|
||||
const auto h = constant(f.enthalpy_binding.size(), h_value);
|
||||
const auto rho = constant(f.density_binding.size(), std::pow(h_value / 6.0, 3.0));
|
||||
const auto u = f.displacement(0.0);
|
||||
PreparedClosure prepared(Closure{eos}, f.density_binding, f.displacement_binding, f.compactification, f.mapper);
|
||||
prepared.prepare(f.state(rho, h), u, f.workspace);
|
||||
::mfem::Vector residual, scale;
|
||||
prepared.residual(f.workspace, residual);
|
||||
const auto zero_h = constant(h.Size(), 0.0);
|
||||
prepared.apply_jacobian(f.state(rho, zero_h), u, f.workspace, scale);
|
||||
REQUIRE(scale.Norml2() > 0.0);
|
||||
CHECK(residual.Norml2() / scale.Norml2() < 5.0e-12);
|
||||
}
|
||||
|
||||
TEST_CASE("Equation Closure Density Action Matches The Stellar Mass Matrix", tags::unit & tags::equations) {
|
||||
for (const auto geometry : {::mfem::Element::HEXAHEDRON, ::mfem::Element::TETRAHEDRON}) {
|
||||
CAPTURE(geometry);
|
||||
Fixture f(geometry);
|
||||
const auto rho = f.density();
|
||||
const auto h = f.enthalpy();
|
||||
const auto zero_h = constant(h.Size(), 0.0);
|
||||
const auto u = f.displacement(0.0);
|
||||
const auto drho = deterministic(rho.Size(), 0.37);
|
||||
PreparedClosure prepared(
|
||||
Closure{eos::models::Polytrope{3.0, 1.5}}, f.density_binding, f.displacement_binding, f.compactification,
|
||||
f.mapper
|
||||
);
|
||||
prepared.prepare(f.state(rho, h), u, f.workspace);
|
||||
::mfem::Vector action;
|
||||
prepared.apply_jacobian(f.state(drho, zero_h), u, f.workspace, action);
|
||||
::mfem::Array<int> marker(3);
|
||||
marker = 0;
|
||||
marker[0] = marker[1] = 1;
|
||||
::mfem::BilinearForm mass(&f.density_space);
|
||||
mass.AddDomainIntegrator(new ::mfem::MassIntegrator(), marker);
|
||||
mass.Assemble();
|
||||
mass.Finalize();
|
||||
const auto full_direction = f.density_binding.scatter(drho);
|
||||
::mfem::Vector full_action(full_direction.Size());
|
||||
mass.Mult(full_direction, full_action);
|
||||
CHECK(relative_error(action, f.density_binding.gather(full_action)) < 5.0e-12);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Equation Closure Thermodynamic Jacobian Matches Centered Differences", tags::unit & tags::equations) {
|
||||
Fixture f;
|
||||
const eos::models::Polytrope eos(3.0, 1.5);
|
||||
const auto rho = f.density();
|
||||
const auto h = f.enthalpy();
|
||||
const auto u = f.displacement(1.0);
|
||||
const auto drho = deterministic(rho.Size(), 0.63);
|
||||
const auto dh = f.project(f.enthalpy_binding, [](const ::mfem::Vector& x) {
|
||||
return 0.07 + 0.015 * x[0] + 0.008 * x[2];
|
||||
});
|
||||
const auto zero_u = constant(u.Size(), 0.0);
|
||||
PreparedClosure prepared(Closure{eos}, f.density_binding, f.displacement_binding, f.compactification, f.mapper);
|
||||
prepared.prepare(f.state(rho, h), u, f.workspace);
|
||||
::mfem::Vector analytic;
|
||||
prepared.apply_jacobian(f.state(drho, dh), zero_u, f.workspace, analytic);
|
||||
constexpr double step = 1.0e-6;
|
||||
auto plus_rho = rho, minus_rho = rho, plus_h = h, minus_h = h;
|
||||
plus_rho.Add(step, drho);
|
||||
minus_rho.Add(-step, drho);
|
||||
plus_h.Add(step, dh);
|
||||
minus_h.Add(-step, dh);
|
||||
auto difference = manual_closure(f, eos, plus_rho, plus_h, u);
|
||||
difference -= manual_closure(f, eos, minus_rho, minus_h, u);
|
||||
difference /= 2.0 * step;
|
||||
CHECK(relative_error(analytic, difference) < 2.0e-8);
|
||||
}
|
||||
|
||||
TEST_CASE("Equation Closure Excludes Vacuum And Uses Mapped Volume", tags::unit & tags::equations) {
|
||||
Fixture f;
|
||||
const auto rho = f.density(), h = f.enthalpy();
|
||||
const auto zero_h = constant(h.Size(), 0.0);
|
||||
const auto zero_u = f.displacement(0.0);
|
||||
const auto ones = constant(rho.Size(), 1.0);
|
||||
::mfem::Vector vacuum(f.density_space.GetVSize());
|
||||
vacuum = 1.0;
|
||||
vacuum -= f.density_binding.scatter(ones);
|
||||
const auto vacuum_direction = f.density_binding.gather(vacuum);
|
||||
CHECK(vacuum.Norml2() > 0.0);
|
||||
::mfem::Vector identity, deformed, vacuum_action;
|
||||
PreparedClosure prepared(
|
||||
Closure{eos::models::Polytrope{3.0, 1.5}}, f.density_binding, f.displacement_binding, f.compactification,
|
||||
f.mapper
|
||||
);
|
||||
prepared.prepare(f.state(rho, h), zero_u, f.workspace);
|
||||
prepared.apply_jacobian(f.state(ones, zero_h), zero_u, f.workspace, identity);
|
||||
prepared.apply_jacobian(f.state(vacuum_direction, zero_h), zero_u, f.workspace, vacuum_action);
|
||||
prepared.prepare(f.state(rho, h), f.displacement(1.0), f.workspace);
|
||||
prepared.apply_jacobian(f.state(ones, zero_h), zero_u, f.workspace, deformed);
|
||||
REQUIRE(identity.Norml2() > 0.0);
|
||||
CHECK(vacuum_action.Norml2() <= 1.0e-13 * identity.Norml2());
|
||||
CHECK(relative_error(deformed, identity) > 1.0e-5);
|
||||
}
|
||||
|
||||
TEST_CASE("Equation Closure Geometry Jacobian Matches Centered Differences", tags::integration & tags::equations) {
|
||||
for (const auto ordering : {::mfem::Ordering::byNODES, ::mfem::Ordering::byVDIM}) {
|
||||
Fixture f(::mfem::Element::HEXAHEDRON, ordering);
|
||||
const eos::models::Polytrope eos(3.0, 1.5);
|
||||
const auto rho = f.density(), h = f.enthalpy(), du = f.displacement(0.65);
|
||||
const auto zero_rho = constant(rho.Size(), 0.0), zero_h = constant(h.Size(), 0.0);
|
||||
for (const double deformation : {0.0, 1.0}) {
|
||||
CAPTURE(ordering, deformation);
|
||||
const auto u = f.displacement(deformation);
|
||||
PreparedClosure prepared(
|
||||
Closure{eos}, f.density_binding, f.displacement_binding, f.compactification, f.mapper
|
||||
);
|
||||
prepared.prepare(f.state(rho, h), u, f.workspace);
|
||||
::mfem::Vector analytic;
|
||||
prepared.apply_jacobian(f.state(zero_rho, zero_h), du, f.workspace, analytic);
|
||||
auto plus = u, minus = u;
|
||||
constexpr double step = 1.0e-5;
|
||||
plus.Add(step, du);
|
||||
minus.Add(-step, du);
|
||||
auto difference = manual_closure(f, eos, rho, h, plus);
|
||||
difference -= manual_closure(f, eos, rho, h, minus);
|
||||
difference /= 2.0 * step;
|
||||
REQUIRE(analytic.Norml2() > 1.0e-12);
|
||||
CHECK(relative_error(analytic, difference) < 5.0e-8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Equation Closure Geometry Action Is Linear And Zero Outside Stellar Support", tags::unit & tags::equations) {
|
||||
Fixture f;
|
||||
const auto rho = f.density(), h = f.enthalpy(), u = f.displacement(0.8);
|
||||
const auto zero_rho = constant(rho.Size(), 0.0), zero_h = constant(h.Size(), 0.0),
|
||||
zero_u = constant(u.Size(), 0.0);
|
||||
const auto first = f.displacement(0.4), second = deterministic(u.Size(), 0.91);
|
||||
auto combined = first;
|
||||
combined *= 1.7;
|
||||
combined.Add(-0.43, second);
|
||||
PreparedClosure prepared(
|
||||
Closure{eos::models::Polytrope{3.0, 1.5}}, f.density_binding, f.displacement_binding, f.compactification,
|
||||
f.mapper
|
||||
);
|
||||
prepared.prepare(f.state(rho, h), u, f.workspace);
|
||||
::mfem::Vector a, b, sum, zero;
|
||||
prepared.apply_jacobian(f.state(zero_rho, zero_h), first, f.workspace, a);
|
||||
prepared.apply_jacobian(f.state(zero_rho, zero_h), second, f.workspace, b);
|
||||
prepared.apply_jacobian(f.state(zero_rho, zero_h), combined, f.workspace, sum);
|
||||
prepared.apply_jacobian(f.state(zero_rho, zero_h), zero_u, f.workspace, zero);
|
||||
a *= 1.7;
|
||||
a.Add(-0.43, b);
|
||||
CHECK(relative_error(sum, a) < 5.0e-12);
|
||||
CHECK(zero.Norml2() == 0.0);
|
||||
auto vacuum_direction = second;
|
||||
::mfem::Array<int> dofs;
|
||||
for (int element = 0; element < f.mesh.GetNE(); ++element) {
|
||||
if (f.mesh.GetAttribute(element) != 3) {
|
||||
f.displacement_space.GetElementVDofs(element, dofs);
|
||||
for (int dof : dofs) {
|
||||
vacuum_direction[dof < 0 ? -1 - dof : dof] = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
REQUIRE(vacuum_direction.Norml2() > 0.0);
|
||||
prepared.apply_jacobian(f.state(zero_rho, zero_h), vacuum_direction, f.workspace, zero);
|
||||
CHECK(zero.Norml2() == 0.0);
|
||||
}
|
||||
} // namespace serif::tests::equations
|
||||
150
tests/equations/closure_prepared.cpp
Normal file
150
tests/equations/closure_prepared.cpp
Normal file
@@ -0,0 +1,150 @@
|
||||
// Prepared-state contracts ported from MeanField's prepared closure and context tests.
|
||||
|
||||
#include <limits>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "serif/tests/equations/fixture.hpp"
|
||||
#include "serif/tests/test_tags.hpp"
|
||||
|
||||
namespace serif::tests::equations {
|
||||
TEST_CASE("Prepared Equations Use Supported Field Dimensions", tags::unit & tags::equations) {
|
||||
Fixture f;
|
||||
PreparedClosure prepared(
|
||||
Closure{eos::models::Polytrope{3.0, 1.5}}, f.density_binding, f.displacement_binding, f.compactification,
|
||||
f.mapper
|
||||
);
|
||||
CHECK_FALSE(prepared.is_prepared());
|
||||
CHECK(prepared.preparation_count() == 0);
|
||||
CHECK(f.density_binding.size() > 0);
|
||||
CHECK(f.density_binding.size() < f.density_space.GetTrueVSize());
|
||||
CHECK(f.enthalpy_binding.size() < f.enthalpy_space.GetTrueVSize());
|
||||
CHECK(f.displacement_binding.size() == f.displacement_space.GetTrueVSize());
|
||||
CHECK(prepared.height() == f.density_binding.size());
|
||||
const auto rho = f.density(), h = f.enthalpy(), u = f.displacement(0.0);
|
||||
prepared.prepare(f.state(rho, h), u, f.workspace);
|
||||
CHECK(prepared.width() == rho.Size() + h.Size() + u.Size());
|
||||
const auto expanded = f.density_binding.scatter(rho);
|
||||
CHECK(relative_error(f.density_binding.gather(expanded), rho) == 0.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Prepared Equations Reject EOS Candidates And Recover", tags::unit & tags::equations) {
|
||||
Fixture f;
|
||||
const auto rho = f.density(), u = f.displacement(0.0);
|
||||
auto h = f.enthalpy();
|
||||
PreparedClosure prepared(
|
||||
Closure{eos::models::Polytrope{3.0, 1.5}}, f.density_binding, f.displacement_binding, f.compactification,
|
||||
f.mapper
|
||||
);
|
||||
prepared.prepare(f.state(rho, h), u, f.workspace);
|
||||
for (const double bad_value : {-1.0, 1.0e150}) {
|
||||
CAPTURE(bad_value);
|
||||
h = bad_value;
|
||||
const auto rejected = prepared.try_prepare(f.state(rho, h), u, f.workspace);
|
||||
REQUIRE_FALSE(rejected.has_value());
|
||||
CHECK(rejected.error().reason == backend::PreparationRejectionReason::EQUATION);
|
||||
CHECK(rejected.error().element >= 0);
|
||||
CHECK(rejected.error().quadrature_point >= 0);
|
||||
CHECK_FALSE(prepared.is_prepared());
|
||||
::mfem::Vector result;
|
||||
CHECK_THROWS_AS(prepared.residual(f.workspace, result), std::logic_error);
|
||||
CHECK_THROWS_AS(prepared.prepare(f.state(rho, h), u, f.workspace), std::domain_error);
|
||||
}
|
||||
h = 1.0;
|
||||
REQUIRE(prepared.try_prepare(f.state(rho, h), u, f.workspace).has_value());
|
||||
CHECK(prepared.preparation_count() == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("Prepared Equations Reject Folding Geometry And Recover", tags::unit & tags::equations) {
|
||||
Fixture f;
|
||||
const auto rho = f.density(), h = f.enthalpy();
|
||||
::mfem::GridFunction folded(&f.displacement_space);
|
||||
::mfem::VectorFunctionCoefficient coefficient(3, [](const ::mfem::Vector& x, ::mfem::Vector& u) {
|
||||
u.SetSize(3);
|
||||
u = 0.0;
|
||||
u[0] = -2.0 * x[0];
|
||||
});
|
||||
folded.ProjectCoefficient(coefficient);
|
||||
auto u = f.displacement_binding.gather(folded);
|
||||
PreparedClosure prepared(
|
||||
Closure{eos::models::Polytrope{3.0, 1.5}}, f.density_binding, f.displacement_binding, f.compactification,
|
||||
f.mapper
|
||||
);
|
||||
const auto rejected = prepared.try_prepare(f.state(rho, h), u, f.workspace);
|
||||
REQUIRE_FALSE(rejected.has_value());
|
||||
CHECK(rejected.error().reason == backend::PreparationRejectionReason::MAPPING);
|
||||
CHECK(rejected.error().mapping_status == mapping::MappingStatus::NON_POSITIVE_DETERMINATE);
|
||||
CHECK_FALSE(prepared.is_prepared());
|
||||
CHECK_THROWS_AS(prepared.prepare(f.state(rho, h), u, f.workspace), std::domain_error);
|
||||
u = 0.0;
|
||||
CHECK(prepared.try_prepare(f.state(rho, h), u, f.workspace).has_value());
|
||||
}
|
||||
|
||||
TEST_CASE("Prepared Closure Matches Manual Residual And Every Jacobian Column", tags::integration & tags::equations) {
|
||||
for (const auto geometry : {::mfem::Element::HEXAHEDRON, ::mfem::Element::TETRAHEDRON}) {
|
||||
CAPTURE(geometry);
|
||||
Fixture f(geometry);
|
||||
const eos::models::Polytrope eos(3.0, 1.5);
|
||||
const auto rho = f.density(), h = f.enthalpy(), u = f.displacement(1.0);
|
||||
const auto drho = deterministic(rho.Size(), 0.43), dh = deterministic(h.Size(), 0.52),
|
||||
du = f.displacement(0.63);
|
||||
const auto zero_rho = constant(rho.Size(), 0.0), zero_h = constant(h.Size(), 0.0),
|
||||
zero_u = constant(u.Size(), 0.0);
|
||||
PreparedClosure prepared(Closure{eos}, f.density_binding, f.displacement_binding, f.compactification, f.mapper);
|
||||
prepared.prepare(f.state(rho, h), u, f.workspace);
|
||||
::mfem::Vector residual, density_action, enthalpy_action, geometry_action, complete, packed_action;
|
||||
prepared.residual(f.workspace, residual);
|
||||
prepared.apply_jacobian(f.state(drho, zero_h), zero_u, f.workspace, density_action);
|
||||
prepared.apply_jacobian(f.state(zero_rho, dh), zero_u, f.workspace, enthalpy_action);
|
||||
prepared.apply_jacobian(f.state(zero_rho, zero_h), du, f.workspace, geometry_action);
|
||||
prepared.apply_jacobian(f.state(drho, dh), du, f.workspace, complete);
|
||||
backend::JacobianOperator jacobian(prepared.cache());
|
||||
jacobian.Mult(packed(drho, dh, du), packed_action);
|
||||
CHECK(relative_error(residual, manual_closure(f, eos, rho, h, u)) < 2.0e-12);
|
||||
CHECK(relative_error(density_action, manual_closure(f, eos, rho, h, u, &drho)) < 2.0e-12);
|
||||
CHECK(relative_error(enthalpy_action, manual_closure(f, eos, rho, h, u, nullptr, &dh)) < 2.0e-12);
|
||||
CHECK(relative_error(geometry_action, manual_closure(f, eos, rho, h, u, nullptr, nullptr, &du)) < 2.0e-12);
|
||||
density_action += enthalpy_action;
|
||||
density_action += geometry_action;
|
||||
CHECK(relative_error(complete, density_action) < 2.0e-12);
|
||||
CHECK(relative_error(packed_action, complete) < 2.0e-12);
|
||||
CHECK(prepared.preparation_count() == 1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Prepared Equations Own Frozen Numerical State And Reprepare Every Dependency", tags::unit & tags::equations) {
|
||||
Fixture f;
|
||||
const eos::models::Polytrope eos(3.0, 1.5);
|
||||
auto rho = f.density(), h = f.enthalpy(), u = f.displacement(0.35);
|
||||
const auto drho = deterministic(rho.Size(), 0.12), dh = deterministic(h.Size(), 0.53),
|
||||
du = f.displacement(0.63);
|
||||
PreparedClosure prepared(Closure{eos}, f.density_binding, f.displacement_binding, f.compactification, f.mapper);
|
||||
prepared.prepare(f.state(rho, h), u, f.workspace);
|
||||
::mfem::Vector frozen, frozen_action;
|
||||
prepared.residual(f.workspace, frozen);
|
||||
prepared.apply_jacobian(f.state(drho, dh), du, f.workspace, frozen_action);
|
||||
for (int dependency = 0; dependency < 3; ++dependency) {
|
||||
CAPTURE(dependency);
|
||||
if (dependency == 0) {
|
||||
rho = f.density(1.0);
|
||||
} else if (dependency == 1) {
|
||||
h = f.enthalpy(1.0);
|
||||
} else {
|
||||
u = f.displacement(0.85);
|
||||
}
|
||||
::mfem::Vector unchanged, unchanged_action;
|
||||
prepared.residual(f.workspace, unchanged);
|
||||
prepared.apply_jacobian(f.state(drho, dh), du, f.workspace, unchanged_action);
|
||||
CHECK(relative_error(unchanged, frozen) == 0.0);
|
||||
CHECK(relative_error(unchanged_action, frozen_action) == 0.0);
|
||||
prepared.prepare(f.state(rho, h), u, f.workspace);
|
||||
prepared.residual(f.workspace, frozen);
|
||||
prepared.apply_jacobian(f.state(drho, dh), du, f.workspace, frozen_action);
|
||||
CHECK(relative_error(frozen, manual_closure(f, eos, rho, h, u)) < 2.0e-12);
|
||||
CHECK(relative_error(frozen_action, manual_closure(f, eos, rho, h, u, &drho, &dh, &du)) < 2.0e-12);
|
||||
}
|
||||
CHECK(prepared.preparation_count() == 4);
|
||||
prepared.invalidate();
|
||||
CHECK_FALSE(prepared.is_prepared());
|
||||
CHECK_THROWS_AS(prepared.residual(f.workspace, frozen), std::logic_error);
|
||||
}
|
||||
} // namespace serif::tests::equations
|
||||
319
tests/equations/computation_graphs.cpp
Normal file
319
tests/equations/computation_graphs.cpp
Normal file
@@ -0,0 +1,319 @@
|
||||
#include "serif/tests/equations/fixture.hpp"
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/generators/catch_generators.hpp>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
using namespace serif;
|
||||
using namespace serif::tests::equations;
|
||||
namespace dependencies = utils::dependencies;
|
||||
namespace stages = backend::volume_keys;
|
||||
|
||||
using Vector = ::mfem::Vector;
|
||||
using Mode = backend::PreparationMode;
|
||||
using Status = dependencies::PreparationStatus;
|
||||
using DensityValue = discretization::forms::Operand<blocks::density::mass>;
|
||||
using EnthalpyValue = discretization::forms::Operand<::serif::tests::equations::Enthalpy>;
|
||||
}
|
||||
|
||||
TEST_CASE("Named volume caches retain independent equation configurations", "[equations][computation_graphs][unit]") {
|
||||
Fixture fixture;
|
||||
const eos::models::Polytrope first_eos{1.5, 0.25};
|
||||
const eos::models::Polytrope second_eos{3.0, 1.5};
|
||||
auto first_density = fixture.density();
|
||||
const auto first_enthalpy = fixture.enthalpy();
|
||||
const auto first_displacement = fixture.displacement(0.04);
|
||||
const auto second_density = fixture.density(1.0);
|
||||
const auto second_enthalpy = fixture.enthalpy(0.5);
|
||||
const auto second_displacement = fixture.displacement(-0.03);
|
||||
|
||||
const backend::VolumeDiscretization discretization{
|
||||
.residual_binding = fixture.density_binding,
|
||||
.displacement_binding = fixture.displacement_binding,
|
||||
.compactification = fixture.compactification,
|
||||
.mapper = fixture.mapper
|
||||
};
|
||||
const auto first_inputs = backend::make_volume_inputs<Closure>(
|
||||
discretization, fixture.state(first_density, first_enthalpy), first_displacement
|
||||
);
|
||||
const auto second_inputs = backend::make_volume_inputs<Closure>(
|
||||
discretization, fixture.state(second_density, second_enthalpy), second_displacement
|
||||
);
|
||||
auto first = backend::make_volume_equation_cache(Closure{first_eos}, discretization, first_inputs);
|
||||
auto second = backend::make_volume_equation_cache(Closure{second_eos}, discretization, second_inputs);
|
||||
CHECK_FALSE(first.ready());
|
||||
CHECK_FALSE(second.ready());
|
||||
|
||||
backend::prepare_volume(first, discretization, fixture.state(first_density, first_enthalpy), first_displacement);
|
||||
backend::prepare_volume(second, discretization, fixture.state(second_density, second_enthalpy), second_displacement);
|
||||
Vector first_residual, second_residual;
|
||||
backend::volume_residual(first.get<stages::Linearization>(), first_residual);
|
||||
backend::volume_residual(second.get<stages::Linearization>(), second_residual);
|
||||
CHECK(relative_error(first_residual, manual_closure(fixture, first_eos, first_density, first_enthalpy, first_displacement)) < 2.0e-12);
|
||||
CHECK(relative_error(second_residual, manual_closure(fixture, second_eos, second_density, second_enthalpy, second_displacement)) < 2.0e-12);
|
||||
|
||||
first_density += 0.07;
|
||||
backend::prepare_volume(first, discretization, fixture.state(first_density, first_enthalpy), first_displacement);
|
||||
backend::volume_residual(first.get<stages::Linearization>(), first_residual);
|
||||
CHECK(relative_error(first_residual, manual_closure(fixture, first_eos, first_density, first_enthalpy, first_displacement)) < 2.0e-12);
|
||||
|
||||
Vector still_second;
|
||||
backend::volume_residual(second.get<stages::Linearization>(), still_second);
|
||||
CHECK(relative_error(still_second, second_residual) == 0.0);
|
||||
CHECK(second.preparation_count() == 1);
|
||||
CHECK(first.preparation_count() == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("Volume graph nodes expose coefficient and geometry reuse independently of workspace storage", "[equations][computation_graphs][unit]") {
|
||||
Fixture fixture;
|
||||
const eos::models::Polytrope eos{1.5, 0.25};
|
||||
auto density = fixture.density();
|
||||
const auto enthalpy = fixture.enthalpy();
|
||||
auto displacement = fixture.displacement(0.04);
|
||||
const backend::VolumeDiscretization discretization{
|
||||
.residual_binding = fixture.density_binding,
|
||||
.displacement_binding = fixture.displacement_binding,
|
||||
.compactification = fixture.compactification,
|
||||
.mapper = fixture.mapper
|
||||
};
|
||||
const auto inputs = backend::make_volume_inputs<Closure>(discretization, fixture.state(density, enthalpy), displacement);
|
||||
auto cache = backend::make_volume_equation_cache(Closure{eos}, discretization, inputs);
|
||||
backend::prepare_volume(cache, discretization, fixture.state(density, enthalpy), displacement);
|
||||
const auto initial = cache.report();
|
||||
CHECK(initial.status<stages::Basis<0>>() == Status::computed);
|
||||
CHECK(initial.status<stages::Geometry<0>>() == Status::computed);
|
||||
CHECK(initial.status<stages::ReferenceValues<0, DensityValue>>() == Status::computed);
|
||||
CHECK(initial.status<stages::ReferenceValues<0, EnthalpyValue>>() == Status::computed);
|
||||
CHECK(initial.status<stages::QuadratureLinearization<0>>() == Status::computed);
|
||||
CHECK(initial.status<stages::GeometryDerivatives<0>>() == Status::computed);
|
||||
const auto original_basis = cache.get<stages::Basis<0>>().front();
|
||||
REQUIRE(original_basis->reference_geometry.size() == static_cast<std::size_t>(original_basis->rule->GetNPoints()));
|
||||
const auto original_derivatives = cache.get<stages::GeometryDerivatives<0>>().front();
|
||||
CHECK(cache.get<stages::Linearization>().geometry_derivatives[0].front() == original_derivatives);
|
||||
|
||||
density += 0.03;
|
||||
backend::prepare_volume(cache, discretization, fixture.state(density, enthalpy), displacement);
|
||||
const auto coefficients_changed = cache.report();
|
||||
CHECK(coefficients_changed.status<stages::Basis<0>>() == Status::reused);
|
||||
CHECK(coefficients_changed.status<stages::Geometry<0>>() == Status::reused);
|
||||
CHECK(coefficients_changed.status<stages::ReferenceValues<0, DensityValue>>() == Status::computed);
|
||||
CHECK(coefficients_changed.status<stages::ReferenceValues<0, EnthalpyValue>>() == Status::reused);
|
||||
CHECK(coefficients_changed.status<stages::QuadratureLinearization<0>>() == Status::computed);
|
||||
CHECK(coefficients_changed.status<stages::GeometryDerivatives<0>>() == Status::computed);
|
||||
CHECK(cache.get<stages::GeometryDerivatives<0>>().front() != original_derivatives);
|
||||
Vector residual;
|
||||
backend::volume_residual(cache.get<stages::Linearization>(), residual);
|
||||
CHECK(relative_error(residual, manual_closure(fixture, eos, density, enthalpy, displacement)) < 2.0e-12);
|
||||
|
||||
displacement *= 1.5;
|
||||
backend::prepare_volume(cache, discretization, fixture.state(density, enthalpy), displacement);
|
||||
const auto geometry_changed = cache.report();
|
||||
CHECK(geometry_changed.status<stages::Basis<0>>() == Status::reused);
|
||||
CHECK(cache.get<stages::Basis<0>>().front() == original_basis);
|
||||
CHECK(geometry_changed.status<stages::Geometry<0>>() == Status::computed);
|
||||
CHECK(geometry_changed.status<stages::ReferenceValues<0, DensityValue>>() == Status::reused);
|
||||
CHECK(geometry_changed.status<stages::ReferenceValues<0, EnthalpyValue>>() == Status::reused);
|
||||
CHECK(geometry_changed.status<stages::QuadratureLinearization<0>>() == Status::computed);
|
||||
CHECK(geometry_changed.status<stages::GeometryDerivatives<0>>() == Status::computed);
|
||||
backend::volume_residual(cache.get<stages::Linearization>(), residual);
|
||||
CHECK(relative_error(residual, manual_closure(fixture, eos, density, enthalpy, displacement)) < 2.0e-12);
|
||||
|
||||
// Equal coefficient copies and different application scratch describe the same numerical state.
|
||||
const Vector density_copy(density), enthalpy_copy(enthalpy), displacement_copy(displacement);
|
||||
backend::Workspace another_workspace;
|
||||
backend::prepare_volume(cache, discretization, fixture.state(density_copy, enthalpy_copy), displacement_copy);
|
||||
const auto unchanged = cache.report();
|
||||
CHECK(unchanged.computed_count() == 0);
|
||||
CHECK(unchanged.status<stages::Linearization>() == Status::reused);
|
||||
CHECK(unchanged.status<stages::GeometryDerivatives<0>>() == Status::reused);
|
||||
Vector repeated;
|
||||
backend::volume_residual(cache.get<stages::Linearization>(), repeated);
|
||||
CHECK(relative_error(repeated, residual) == 0.0);
|
||||
|
||||
// Application reads accepted snapshots until the caller explicitly prepares new inputs.
|
||||
const auto direction = packed(fixture.density(0.7), fixture.enthalpy(0.2), fixture.displacement(0.01));
|
||||
const auto dual = deterministic(fixture.density_binding.size(), 0.3);
|
||||
Vector action, transpose, frozen_action, frozen_transpose;
|
||||
backend::apply_jacobian(cache.get<stages::Linearization>(), direction, fixture.workspace, action);
|
||||
backend::apply_jacobian_transpose(cache.get<stages::Linearization>(), dual, fixture.workspace, transpose);
|
||||
density += 0.09;
|
||||
displacement *= 1.1;
|
||||
backend::volume_residual(cache.get<stages::Linearization>(), repeated);
|
||||
backend::apply_jacobian(cache.get<stages::Linearization>(), direction, another_workspace, frozen_action);
|
||||
backend::apply_jacobian_transpose(cache.get<stages::Linearization>(), dual, another_workspace, frozen_transpose);
|
||||
CHECK(relative_error(repeated, residual) == 0.0);
|
||||
CHECK(relative_error(frozen_action, action) == 0.0);
|
||||
CHECK(relative_error(frozen_transpose, transpose) == 0.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Rejected volume graph candidates never expose partially updated operator results", "[equations][computation_graphs][unit]") {
|
||||
Fixture fixture;
|
||||
const eos::models::Polytrope eos{1.5, 0.25};
|
||||
auto density = fixture.density();
|
||||
auto enthalpy = fixture.enthalpy();
|
||||
auto displacement = fixture.displacement(0.04);
|
||||
const backend::VolumeDiscretization discretization{
|
||||
.residual_binding = fixture.density_binding,
|
||||
.displacement_binding = fixture.displacement_binding,
|
||||
.compactification = fixture.compactification,
|
||||
.mapper = fixture.mapper
|
||||
};
|
||||
const auto inputs = backend::make_volume_inputs<Closure>(discretization, fixture.state(density, enthalpy), displacement);
|
||||
auto cache = backend::make_volume_equation_cache(Closure{eos}, discretization, inputs);
|
||||
backend::prepare_volume(cache, discretization, fixture.state(density, enthalpy), displacement);
|
||||
|
||||
// Several upstream results change before the equation rejects this candidate.
|
||||
density += 0.05;
|
||||
displacement *= 1.5;
|
||||
enthalpy = -1.0;
|
||||
const auto rejected = backend::try_prepare_volume(cache, discretization, fixture.state(density, enthalpy), displacement);
|
||||
REQUIRE_FALSE(rejected.has_value());
|
||||
CHECK(rejected.error().reason == backend::PreparationRejectionReason::EQUATION);
|
||||
CHECK_FALSE(cache.ready());
|
||||
CHECK_THROWS_AS(cache.get<stages::Linearization>(), std::logic_error);
|
||||
CHECK_THROWS_AS(cache.get<stages::Basis<0>>(), std::logic_error);
|
||||
CHECK_THROWS_AS(cache.get<stages::GeometryDerivatives<0>>(), std::logic_error);
|
||||
Vector result;
|
||||
CHECK_THROWS_AS(backend::volume_residual(cache.get<stages::Linearization>(), result), std::logic_error);
|
||||
|
||||
enthalpy = fixture.enthalpy(0.5);
|
||||
backend::prepare_volume(cache, discretization, fixture.state(density, enthalpy), displacement);
|
||||
backend::volume_residual(cache.get<stages::Linearization>(), result);
|
||||
CHECK(relative_error(result, manual_closure(fixture, eos, density, enthalpy, displacement)) < 2.0e-12);
|
||||
CHECK(cache.preparation_count() == 2);
|
||||
|
||||
backend::prepare_volume(cache, discretization, fixture.state(density, enthalpy), displacement, Mode::rebuild);
|
||||
Vector rebuilt;
|
||||
backend::volume_residual(cache.get<stages::Linearization>(), rebuilt);
|
||||
CHECK(relative_error(result, rebuilt) < 2.0e-15);
|
||||
}
|
||||
|
||||
TEST_CASE("Volume caches reject another discretization of the same type and recover", "[equations][computation_graphs][unit]") {
|
||||
Fixture fixture;
|
||||
Fixture other_fixture;
|
||||
const eos::models::Polytrope eos{1.5, 0.25};
|
||||
const auto density = fixture.density();
|
||||
const auto enthalpy = fixture.enthalpy();
|
||||
const auto displacement = fixture.displacement(0.04);
|
||||
const auto other_density = other_fixture.density(0.5);
|
||||
const auto other_enthalpy = other_fixture.enthalpy(0.7);
|
||||
const auto other_displacement = other_fixture.displacement(-0.03);
|
||||
const backend::VolumeDiscretization discretization{
|
||||
.residual_binding = fixture.density_binding,
|
||||
.displacement_binding = fixture.displacement_binding,
|
||||
.compactification = fixture.compactification,
|
||||
.mapper = fixture.mapper
|
||||
};
|
||||
const backend::VolumeDiscretization other_discretization{
|
||||
.residual_binding = other_fixture.density_binding,
|
||||
.displacement_binding = other_fixture.displacement_binding,
|
||||
.compactification = other_fixture.compactification,
|
||||
.mapper = other_fixture.mapper
|
||||
};
|
||||
const auto inputs = backend::make_volume_inputs<Closure>(discretization, fixture.state(density, enthalpy), displacement);
|
||||
auto cache = backend::make_volume_equation_cache(Closure{eos}, discretization, inputs);
|
||||
backend::prepare_volume(cache, discretization, fixture.state(density, enthalpy), displacement);
|
||||
CHECK(cache.preparation_count() == 1);
|
||||
|
||||
CHECK_THROWS_AS(
|
||||
backend::try_prepare_volume(cache, other_discretization, other_fixture.state(other_density, other_enthalpy), other_displacement),
|
||||
std::invalid_argument
|
||||
);
|
||||
CHECK_FALSE(cache.ready());
|
||||
CHECK_THROWS_AS(cache.get<stages::Linearization>(), std::logic_error);
|
||||
CHECK_THROWS_AS(cache.get<stages::Basis<0>>(), std::logic_error);
|
||||
CHECK_THROWS_AS(cache.get<stages::GeometryDerivatives<0>>(), std::logic_error);
|
||||
CHECK(cache.preparation_count() == 1);
|
||||
|
||||
backend::prepare_volume(cache, discretization, fixture.state(density, enthalpy), displacement);
|
||||
Vector residual;
|
||||
backend::volume_residual(cache.get<stages::Linearization>(), residual);
|
||||
CHECK(relative_error(residual, manual_closure(fixture, eos, density, enthalpy, displacement)) < 2.0e-12);
|
||||
CHECK(cache.preparation_count() == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("The volume graph refreshes immutable reference geometry after mesh coordinate updates", "[equations][computation_graphs][unit]") {
|
||||
Fixture fixture;
|
||||
const eos::models::Polytrope eos{1.5, 0.25};
|
||||
const auto density = fixture.density();
|
||||
const auto enthalpy = fixture.enthalpy();
|
||||
const auto displacement = fixture.displacement(0.04);
|
||||
const backend::VolumeDiscretization discretization{
|
||||
.residual_binding = fixture.density_binding,
|
||||
.displacement_binding = fixture.displacement_binding,
|
||||
.compactification = fixture.compactification,
|
||||
.mapper = fixture.mapper
|
||||
};
|
||||
const auto inputs = backend::make_volume_inputs<Closure>(discretization, fixture.state(density, enthalpy), displacement);
|
||||
auto cache = backend::make_volume_equation_cache(Closure{eos}, discretization, inputs);
|
||||
backend::prepare_volume(cache, discretization, fixture.state(density, enthalpy), displacement);
|
||||
const auto original = cache.get<stages::Basis<0>>().front();
|
||||
const double original_weight = original->reference_geometry.front().weight;
|
||||
|
||||
fixture.mesh.Transform([](const ::mfem::Vector& position, ::mfem::Vector& transformed) {
|
||||
transformed = position;
|
||||
transformed[0] *= 1.2;
|
||||
});
|
||||
backend::prepare_volume(cache, discretization, fixture.state(density, enthalpy), displacement);
|
||||
CHECK(cache.report().status<stages::Basis<0>>() == Status::computed);
|
||||
const auto changed = cache.get<stages::Basis<0>>().front();
|
||||
CHECK(changed != original);
|
||||
CHECK(changed->reference_geometry.front().weight > original_weight);
|
||||
CHECK(original->reference_geometry.front().weight == original_weight);
|
||||
Vector residual;
|
||||
backend::volume_residual(cache.get<stages::Linearization>(), residual);
|
||||
CHECK(relative_error(residual, manual_closure(fixture, eos, density, enthalpy, displacement)) < 2.0e-12);
|
||||
|
||||
fixture.mesh.Transform([](const ::mfem::Vector& position, ::mfem::Vector& transformed) {
|
||||
transformed = position;
|
||||
transformed[0] = 0.0;
|
||||
});
|
||||
const auto rejected = backend::try_prepare_volume(cache, discretization, fixture.state(density, enthalpy), displacement);
|
||||
REQUIRE_FALSE(rejected);
|
||||
CHECK(rejected.error().reason == backend::PreparationRejectionReason::MAPPING);
|
||||
CHECK_FALSE(cache.ready());
|
||||
CHECK(original->reference_geometry.front().weight == original_weight);
|
||||
}
|
||||
|
||||
TEST_CASE("Volume graph source tracking rejects stale reused input contexts", "[equations][computation_graphs][unit]") {
|
||||
const bool initially_prepared = GENERATE(false, true);
|
||||
Fixture fixture;
|
||||
const eos::models::Polytrope eos{1.5, 0.25};
|
||||
const auto density = fixture.density();
|
||||
const auto enthalpy = fixture.enthalpy();
|
||||
const auto displacement = fixture.displacement(0.04);
|
||||
const backend::VolumeDiscretization discretization{
|
||||
.residual_binding = fixture.density_binding,
|
||||
.displacement_binding = fixture.displacement_binding,
|
||||
.compactification = fixture.compactification,
|
||||
.mapper = fixture.mapper
|
||||
};
|
||||
const auto inputs = backend::make_volume_inputs<Closure>(discretization, fixture.state(density, enthalpy), displacement);
|
||||
auto cache = backend::make_volume_equation_cache(Closure{eos}, discretization, inputs);
|
||||
if (initially_prepared) {
|
||||
REQUIRE(cache.try_prepare(inputs));
|
||||
}
|
||||
SECTION("Coordinate changes require fresh source metadata") {
|
||||
fixture.mesh.NodesUpdated();
|
||||
CHECK_THROWS_AS(cache.try_prepare(inputs), std::logic_error);
|
||||
CHECK_FALSE(cache.ready());
|
||||
const auto current = backend::make_volume_inputs<Closure>(discretization, fixture.state(density, enthalpy), displacement);
|
||||
REQUIRE(cache.try_prepare(current));
|
||||
Vector residual;
|
||||
backend::volume_residual(cache.get<stages::Linearization>(), residual);
|
||||
CHECK(relative_error(residual, manual_closure(fixture, eos, density, enthalpy, displacement)) < 2.0e-12);
|
||||
}
|
||||
SECTION("Domain edits invalidate the borrowed bindings") {
|
||||
fixture.mesh.GetElement(0)->SetAttribute(3);
|
||||
CHECK_THROWS_AS(cache.try_prepare(inputs), std::logic_error);
|
||||
CHECK_FALSE(cache.ready());
|
||||
}
|
||||
SECTION("Topology edits invalidate the borrowed bindings") {
|
||||
fixture.mesh.UniformRefinement();
|
||||
CHECK_THROWS_AS(cache.try_prepare(inputs), std::logic_error);
|
||||
CHECK_FALSE(cache.ready());
|
||||
}
|
||||
}
|
||||
77
tests/equations/contribution_lifetimes.cpp
Normal file
77
tests/equations/contribution_lifetimes.cpp
Normal file
@@ -0,0 +1,77 @@
|
||||
#include <memory>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "serif/discretization/blocks/fields.hpp"
|
||||
#include "serif/equations/concepts.hpp"
|
||||
#include "serif/operators/contributions.hpp"
|
||||
|
||||
namespace {
|
||||
/// Record exactly when an equation's owned numerical coefficient is destroyed.
|
||||
struct TrackedCoefficient {
|
||||
double value;
|
||||
std::shared_ptr<int> destructions;
|
||||
|
||||
/// Construct the tracked value without an intermediate temporary.
|
||||
TrackedCoefficient(double coefficient, std::shared_ptr<int> count) : value(coefficient), destructions(std::move(count)) {}
|
||||
/// Release the observable coefficient lifetime.
|
||||
~TrackedCoefficient() { ++*destructions; }
|
||||
};
|
||||
|
||||
/// A move-only leaf prevents traversal from concealing lifetime mistakes through equation copies.
|
||||
struct TrackedEquation {
|
||||
using Test = serif::discretization::blocks::density::mass;
|
||||
using Density = serif::discretization::forms::Operand<Test>;
|
||||
using Operands = serif::utils::types::TypeList<Density>;
|
||||
static constexpr std::string_view name = "tracked equation";
|
||||
|
||||
std::unique_ptr<TrackedCoefficient> slope;
|
||||
|
||||
/// Own a numerical coefficient whose destruction remains observable outside this equation.
|
||||
TrackedEquation(double coefficient, const std::shared_ptr<int>& count) : slope(std::make_unique<TrackedCoefficient>(coefficient, count)) {}
|
||||
|
||||
/// Evaluate a linear residual using the retained coefficient.
|
||||
[[nodiscard]] serif::equations::PointLinearization<Operands> evaluate(const serif::equations::EquationPoint<Operands>& point) const {
|
||||
serif::equations::PointLinearization<Operands> result;
|
||||
result.residual = slope->value * point.fields.get<Density>();
|
||||
result.field_derivatives.get<Density>() = slope->value;
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
/// Generate a nonempty move-only contribution tuple by value, as mixed gravity generates its leaves.
|
||||
struct GeneratedEquation {
|
||||
using Test = TrackedEquation::Test;
|
||||
static constexpr std::string_view name = "generated contribution";
|
||||
double slope;
|
||||
std::shared_ptr<int> destructions;
|
||||
|
||||
/// The caller must retain this tuple if it wants to retain references to its equation.
|
||||
[[nodiscard]] auto contributions() const { return std::tuple{TrackedEquation{slope, destructions}}; }
|
||||
};
|
||||
|
||||
static_assert(serif::equations::Equation<GeneratedEquation>);
|
||||
static_assert(!std::is_copy_constructible_v<TrackedEquation>);
|
||||
}
|
||||
|
||||
TEST_CASE("Contribution bindings retain generated tuples and borrowed move-only leaves", "[equations][contributions][unit]") {
|
||||
using namespace serif::equations;
|
||||
|
||||
const auto destructions = std::make_shared<int>(0);
|
||||
auto owner = std::make_shared<const GeneratedEquation>(3.0, destructions);
|
||||
{
|
||||
const auto leaves = serif::operators::bind_contributions(owner);
|
||||
STATIC_CHECK(std::tuple_size_v<decltype(leaves)> == 1);
|
||||
owner.reset();
|
||||
|
||||
// The generated tuple remains alive after its parent equation is released.
|
||||
REQUIRE(*destructions == 0);
|
||||
CHECK(std::get<0>(leaves).coefficient == 1.0);
|
||||
EquationPoint<TrackedEquation::Operands> point;
|
||||
point.fields.get<TrackedEquation::Density>() = 4.0;
|
||||
CHECK(std::get<0>(leaves).equation->evaluate(point).residual == 12.0);
|
||||
}
|
||||
CHECK(*destructions == 1);
|
||||
}
|
||||
91
tests/equations/equation_contracts.cpp
Normal file
91
tests/equations/equation_contracts.cpp
Normal file
@@ -0,0 +1,91 @@
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
|
||||
#include "serif/equations/primal_gravity.hpp"
|
||||
#include "serif/equations/registry.hpp"
|
||||
#include "serif/tests/equations/fixture.hpp"
|
||||
#include "serif/tests/test_tags.hpp"
|
||||
|
||||
namespace serif::tests::equations {
|
||||
// An external-style EOS needs only the advertised relation and derivative.
|
||||
struct LinearEOS final {
|
||||
using Relations = eos::relations::RelationCatalog<eos::relations::DensityFromSpecificEnthalpy>;
|
||||
std::shared_ptr<int> evaluations;
|
||||
|
||||
dimensions::DensityValue evaluate(
|
||||
eos::relations::DensityFromSpecificEnthalpy,
|
||||
dimensions::SpecificEnthalpyValue h
|
||||
) const {
|
||||
++*evaluations;
|
||||
return dimensions::DensityValue{2.0 * h.value()};
|
||||
}
|
||||
|
||||
dimensions::PartialDerivative<dimensions::MassDensity, dimensions::SpecificEnthalpy> partial_derivative(
|
||||
eos::relations::DensityFromSpecificEnthalpy,
|
||||
dimensions::WithRespectTo<dimensions::SpecificEnthalpy>,
|
||||
dimensions::SpecificEnthalpyValue
|
||||
) const {
|
||||
return dimensions::PartialDerivative<dimensions::MassDensity, dimensions::SpecificEnthalpy>{2.0};
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CASE("Equation Closure Accepts Compatible EOS Without Backend Specialization", tags::unit & tags::equations) {
|
||||
Fixture f;
|
||||
const auto calls = std::make_shared<int>(0);
|
||||
const auto equation = ::serif::tests::equations::BarotropicClosure{LinearEOS{calls}, 1.0};
|
||||
VolumeEquationFixture prepared(
|
||||
equation, f.density_binding, f.displacement_binding, f.compactification, f.mapper
|
||||
);
|
||||
const auto rho = constant(f.density_binding.size(), 1.6), h = constant(f.enthalpy_binding.size(), 0.8);
|
||||
const auto u = f.displacement(0.8);
|
||||
prepared.prepare(f.state(rho, h), u, f.workspace);
|
||||
const int preparation_calls = *calls;
|
||||
REQUIRE(preparation_calls > 0);
|
||||
::mfem::Vector residual, action;
|
||||
prepared.residual(f.workspace, residual);
|
||||
prepared.apply_jacobian(f.state(rho, h), u, f.workspace, action);
|
||||
CHECK(residual.Norml2() < 1.0e-13);
|
||||
CHECK(action.Norml2() < 1.0e-13);
|
||||
CHECK(*calls == preparation_calls);
|
||||
}
|
||||
|
||||
TEST_CASE("Equation Closure Local Derivatives Match The Analytic Polytrope", tags::unit & tags::equations) {
|
||||
for (const double n : {1.0, 1.5, 3.0}) {
|
||||
Closure equation{eos::models::Polytrope{n, 1.5}};
|
||||
serif::equations::EquationPoint<Closure::Operands> point;
|
||||
point.fields.get<blocks::density::mass>() = 0.55;
|
||||
point.fields.get<::serif::tests::equations::Enthalpy>() = 0.9;
|
||||
const auto result = equation.evaluate(point);
|
||||
const double scale = (n + 1.0) * 1.5;
|
||||
CHECK_THAT(result.residual, Catch::Matchers::WithinRel(0.55 - std::pow(0.9 / scale, n), 1.0e-14));
|
||||
CHECK(result.field_derivatives.get<blocks::density::mass>() == 1.0);
|
||||
CHECK_THAT(
|
||||
result.field_derivatives.get<::serif::tests::equations::Enthalpy>(),
|
||||
Catch::Matchers::WithinRel(-n / scale * std::pow(0.9 / scale, n - 1.0), 1.0e-14)
|
||||
);
|
||||
CHECK(result.position_derivative == std::array<double, 3>{});
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Equation Registry Defines Rows From Its Equations", tags::unit & tags::equations) {
|
||||
const serif::equations::EquationRegistry registry{
|
||||
Closure{eos::models::Polytrope{1.0, 0.8}}, serif::equations::PrimalPoissonBalance{1.0}
|
||||
};
|
||||
int count = 0;
|
||||
registry.for_each([&](const auto& equation) {
|
||||
CHECK_FALSE(equation.name.empty());
|
||||
++count;
|
||||
});
|
||||
CHECK(count == 2);
|
||||
CHECK(registry.get<Closure>().name == "barotropic closure");
|
||||
CHECK_THROWS_AS(
|
||||
(::serif::tests::equations::BarotropicClosure{LinearEOS{std::make_shared<int>(0)}, -1.0}), std::invalid_argument
|
||||
);
|
||||
CHECK_THROWS_AS(
|
||||
(serif::equations::PrimalPoissonBalance{std::numeric_limits<double>::infinity()}),
|
||||
std::invalid_argument
|
||||
);
|
||||
}
|
||||
} // namespace serif::tests::equations
|
||||
106
tests/equations/fixture.cpp
Normal file
106
tests/equations/fixture.cpp
Normal file
@@ -0,0 +1,106 @@
|
||||
#include "serif/tests/equations/fixture.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
|
||||
namespace serif::tests::equations {
|
||||
::mfem::Mesh make_mesh(::mfem::Element::Type geometry) {
|
||||
auto mesh = ::mfem::Mesh::MakeCartesian3D(3, 1, 1, geometry, 1.5, 1.0, 1.0);
|
||||
::mfem::Vector center(3);
|
||||
for (int element = 0; element < mesh.GetNE(); ++element) {
|
||||
auto* transformation = mesh.GetElementTransformation(element);
|
||||
transformation->Transform(::mfem::Geometries.GetCenter(transformation->GetGeometryType()), center);
|
||||
mesh.GetElement(element)->SetAttribute(1 + static_cast<int>(center[0] / 0.5));
|
||||
}
|
||||
mesh.SetAttributes();
|
||||
return mesh;
|
||||
}
|
||||
|
||||
::mfem::Vector constant(int size, double value) {
|
||||
::mfem::Vector result(size);
|
||||
result = value;
|
||||
return result;
|
||||
}
|
||||
|
||||
::mfem::Vector deterministic(int size, double phase) {
|
||||
::mfem::Vector result(size);
|
||||
for (int i = 0; i < size; ++i) {
|
||||
result[i] = 0.1 * std::sin(0.13 * i + phase);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
::mfem::Vector packed(
|
||||
const ::mfem::Vector& density,
|
||||
const ::mfem::Vector& enthalpy,
|
||||
const ::mfem::Vector& displacement
|
||||
) {
|
||||
::mfem::Vector result(density.Size() + enthalpy.Size() + displacement.Size());
|
||||
std::copy_n(density.GetData(), density.Size(), result.GetData());
|
||||
std::copy_n(enthalpy.GetData(), enthalpy.Size(), result.GetData() + density.Size());
|
||||
std::copy_n(displacement.GetData(), displacement.Size(), result.GetData() + density.Size() + enthalpy.Size());
|
||||
return result;
|
||||
}
|
||||
|
||||
double relative_error(const ::mfem::Vector& actual, const ::mfem::Vector& expected) {
|
||||
::mfem::Vector difference(actual);
|
||||
difference -= expected;
|
||||
return difference.Norml2() / std::max(expected.Norml2(), 1.0e-30);
|
||||
}
|
||||
|
||||
Fixture::Fixture(::mfem::Element::Type geometry, ::mfem::Ordering::Type ordering) : Fixture(make_mesh(geometry), ordering) {}
|
||||
|
||||
Fixture::Fixture(::mfem::Mesh supplied_mesh, ::mfem::Ordering::Type ordering) :
|
||||
mesh(std::move(supplied_mesh)),
|
||||
density_space(&mesh, &density_collection),
|
||||
enthalpy_space(&mesh, &enthalpy_collection),
|
||||
potential_space(&mesh, &potential_collection),
|
||||
displacement_space(&mesh, &enthalpy_collection, 3, ordering),
|
||||
compactification(&enthalpy_space),
|
||||
density_binding(density_space),
|
||||
enthalpy_binding(enthalpy_space),
|
||||
displacement_binding(displacement_space),
|
||||
mapper(
|
||||
{},
|
||||
std::make_unique<mapping::compactification::KelvinCompactification>(
|
||||
utils::user_options::KelvinCompactificationOptions{.r_star_ref = 1.0, .r_inf_ref = 4.0}
|
||||
)
|
||||
) {
|
||||
compactification = 0.0;
|
||||
}
|
||||
|
||||
::mfem::Vector Fixture::project(
|
||||
const discretization::backend::mfem::DofMap& field,
|
||||
std::function<double(const ::mfem::Vector&)> function
|
||||
) {
|
||||
::mfem::FunctionCoefficient coefficient(std::move(function));
|
||||
::mfem::GridFunction values(&field.space());
|
||||
values.ProjectCoefficient(coefficient);
|
||||
return field.gather(values);
|
||||
}
|
||||
|
||||
::mfem::Vector Fixture::density(double phase) {
|
||||
return project(density_binding, [phase](const ::mfem::Vector& x) {
|
||||
return 0.55 + 0.025 * x[0] - 0.010 * x[1] + 0.006 * x[2] + 0.03 * phase;
|
||||
});
|
||||
}
|
||||
|
||||
::mfem::Vector Fixture::enthalpy(double phase) {
|
||||
return project(enthalpy_binding, [phase](const ::mfem::Vector& x) {
|
||||
return 0.90 + 0.020 * x[0] - 0.010 * x[1] + 0.005 * x[2] + 0.02 * phase;
|
||||
});
|
||||
}
|
||||
|
||||
::mfem::Vector Fixture::displacement(double scale) {
|
||||
::mfem::VectorFunctionCoefficient coefficient(3, [scale](const ::mfem::Vector& x, ::mfem::Vector& u) {
|
||||
u.SetSize(3);
|
||||
u[0] = scale * (0.03 * x[0] + 0.02 * x[0] * x[1]);
|
||||
u[1] = scale * (-0.02 * x[1] + 0.01 * x[0] * x[2]);
|
||||
u[2] = scale * (0.04 * x[2] - 0.015 * x[0] * x[1]);
|
||||
});
|
||||
::mfem::GridFunction values(&displacement_space);
|
||||
values.ProjectCoefficient(coefficient);
|
||||
return displacement_binding.gather(values);
|
||||
}
|
||||
} // namespace serif::tests::equations
|
||||
165
tests/equations/gravity_port_fixture.cpp
Normal file
165
tests/equations/gravity_port_fixture.cpp
Normal file
@@ -0,0 +1,165 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
#include <stroid/stroid.h>
|
||||
#include "serif/tests/equations/gravity_port.hpp"
|
||||
|
||||
namespace serif::tests::equations::gravity_port {
|
||||
/// Project an old test polynomial into the same registered coefficient representation.
|
||||
Vector project_vector(const discretization::backend::mfem::DofMap& binding, std::function<void(const Vector&, Vector&)> function) {
|
||||
::mfem::VectorFunctionCoefficient coefficient(3, std::move(function));
|
||||
::mfem::GridFunction values(&binding.space());
|
||||
values.ProjectCoefficient(coefficient);
|
||||
return binding.gather(values);
|
||||
}
|
||||
Coefficients zeros(const GravityFixture& f) {
|
||||
return {constant(f.potential_binding.size(), 0.0),
|
||||
constant(f.density_binding.size(), 0.0), constant(f.displacement_binding.size(), 0.0)};
|
||||
}
|
||||
Vector sequence(int size, double scale, double phase) {
|
||||
Vector v(size);
|
||||
for (int i = 0; i < size; ++i) {
|
||||
const double index = i + 1;
|
||||
v[i] = scale * (std::sin(0.31 * index + phase) + 0.37 * std::cos(0.17 * index - phase));
|
||||
}
|
||||
return v;
|
||||
}
|
||||
Vector prepared_sequence(int size, double phase) {
|
||||
Vector v(size);
|
||||
for (int i = 0; i < size; ++i) {
|
||||
const double index = i + 1;
|
||||
v[i] = std::sin(0.37 * index + phase) + 0.31 * std::cos(0.19 * index - 0.5 * phase);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
Vector operator_sequence(int size, double phase) {
|
||||
Vector v(size);
|
||||
for (int i = 0; i < size; ++i) {
|
||||
const double index = i + 1;
|
||||
v[i] = 0.4 * std::sin(0.37 * index + phase) + 0.2 * std::cos(0.19 * index - phase);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
Coefficients state(GravityFixture& f, bool deformed) {
|
||||
return {sequence(f.potential_binding.size(), 0.3, 0.37),
|
||||
sequence(f.density_binding.size(), 0.7, 0.11), prepared_displacement(f, deformed ? 1.0 : 0.0)};
|
||||
}
|
||||
Coefficients direction(GravityFixture& f, int column) {
|
||||
auto d = zeros(f);
|
||||
if (column == 0) {
|
||||
d.density = sequence(d.density.Size(), 0.13, 0.19);
|
||||
} else {
|
||||
d.potential = sequence(d.potential.Size(), 0.09, 0.41);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
Vector prepared_displacement(GravityFixture& f, double scale) {
|
||||
return project_vector(f.displacement_binding, [scale](const Vector& x, Vector& u) {
|
||||
u.SetSize(3);
|
||||
u[0] = scale * (0.04 * x[0] + 0.01 * x[1] * x[2]);
|
||||
u[1] = scale * (-0.03 * x[1] + 0.008 * x[0] * x[2]);
|
||||
u[2] = scale * (0.02 * x[2] - 0.006 * x[0] * x[1]);
|
||||
});
|
||||
}
|
||||
Vector affine_displacement(GravityFixture& f, double scale) {
|
||||
return project_vector(f.displacement_binding, [scale](const Vector& x, Vector& u) {
|
||||
u = x;
|
||||
u *= scale;
|
||||
});
|
||||
}
|
||||
Vector kernel_displacement(GravityFixture& f) {
|
||||
return project_vector(f.displacement_binding, [](const Vector& x, Vector& u) {
|
||||
u.SetSize(3);
|
||||
u[0] = 0.025 * x[0] + 0.006 * x[1] * x[2];
|
||||
u[1] = -0.018 * x[1] + 0.005 * x[0] * x[2];
|
||||
u[2] = 0.014 * x[2] + 0.004 * x[0] * x[1];
|
||||
});
|
||||
}
|
||||
Vector geometry_direction(GravityFixture& f, int index) {
|
||||
return project_vector(f.displacement_binding, [index](const Vector& x, Vector& u) {
|
||||
u.SetSize(3);
|
||||
if (index == 0) {
|
||||
u[0] = 0.16 * x[0] + 0.03 * x[1];
|
||||
u[1] = -0.11 * x[1] + 0.02 * x[2];
|
||||
u[2] = 0.13 * x[2] - 0.025 * x[0];
|
||||
} else {
|
||||
u[0] = -0.07 * x[1] + 0.025 * x[2];
|
||||
u[1] = 0.09 * x[0] + 0.04 * x[2];
|
||||
u[2] = -0.08 * x[2] + 0.03 * x[0] * x[1];
|
||||
}
|
||||
});
|
||||
}
|
||||
Vector kernel_density(GravityFixture& f, bool secondary) {
|
||||
return f.project(f.density_binding, [secondary](const Vector& x) {
|
||||
return secondary ? 0.8 - 0.11 * x[0] + 0.13 * x[1] - 0.06 * x[2] + 0.03 * x[1] * x[2]
|
||||
: 1.2 + 0.16 * x[0] - 0.09 * x[1] + 0.07 * x[2] + 0.04 * x[0] * x[1];
|
||||
});
|
||||
}
|
||||
Vector domain_density(GravityFixture& f, bool stellar) {
|
||||
Vector density(f.density_space.GetVSize());
|
||||
std::vector<bool> stellar_dofs(density.Size(), false);
|
||||
::mfem::Array<int> dofs;
|
||||
for (int element = 0; element < f.mesh.GetNE(); ++element) {
|
||||
if (!Schema::template domain_id_belongs_to<discretization::domain::StellarDomains>(f.mesh.GetAttribute(element))) {
|
||||
continue;
|
||||
}
|
||||
f.density_space.GetElementDofs(element, dofs);
|
||||
for (const int dof : dofs) {
|
||||
stellar_dofs[dof] = true;
|
||||
}
|
||||
}
|
||||
for (int dof = 0; dof < density.Size(); ++dof) {
|
||||
density[dof] = stellar_dofs[dof] == stellar ? 1.0 : 0.0;
|
||||
}
|
||||
return density;
|
||||
}
|
||||
std::unique_ptr<GravityFixture> stellar_fixture(int refinement_levels, int geometry_order) {
|
||||
stroid::config::MeshConfig config;
|
||||
config.refinement_levels = refinement_levels;
|
||||
config.order = geometry_order;
|
||||
config.include_external_domain = true;
|
||||
config.r_core = 0.25;
|
||||
config.r_star = 1.0;
|
||||
config.r_infinity = 4.0;
|
||||
config.flattening = 0.0;
|
||||
config.optimization_methods = stroid::config::OptimizationMethods{false, true};
|
||||
auto mesh = stroid::GenerateMesh(config);
|
||||
REQUIRE(mesh.exterior_coordinate != nullptr);
|
||||
REQUIRE(mesh.exterior_coordinate->values != nullptr);
|
||||
auto fixture = std::make_unique<GravityFixture>(::mfem::Mesh(*mesh.mesh));
|
||||
::mfem::GridFunctionCoefficient coordinate(mesh.exterior_coordinate->values.get());
|
||||
fixture->compactification.ProjectCoefficient(coordinate);
|
||||
return fixture;
|
||||
}
|
||||
Coefficients shifted(const Coefficients& v, const Coefficients& d, double scale) {
|
||||
auto result = v;
|
||||
result.potential.Add(scale, d.potential);
|
||||
result.density.Add(scale, d.density);
|
||||
result.displacement.Add(scale, d.displacement);
|
||||
return result;
|
||||
}
|
||||
double relative(const Vector& actual, const Vector& expected, double floor) {
|
||||
REQUIRE(actual.Size() == expected.Size());
|
||||
Vector difference(actual);
|
||||
difference -= expected;
|
||||
return difference.Norml2() / std::max(expected.Norml2(), floor);
|
||||
}
|
||||
double symmetric_relative(const Vector& first, const Vector& second) {
|
||||
REQUIRE(first.Size() == second.Size());
|
||||
Vector difference(first);
|
||||
difference -= second;
|
||||
return difference.Norml2() / std::max({first.Norml2(), second.Norml2(), 1.0e-14});
|
||||
}
|
||||
double prepared_relative(const Vector& actual, const Vector& expected) {
|
||||
return relative(actual, expected, std::numeric_limits<double>::epsilon());
|
||||
}
|
||||
double pairing_relative(double actual, double expected) {
|
||||
return std::abs(actual - expected) / std::max(std::abs(expected), std::numeric_limits<double>::epsilon());
|
||||
}
|
||||
Vector combination(const Vector& first, double a, const Vector& second, double b) {
|
||||
Vector result(first);
|
||||
result *= a;
|
||||
result.Add(b, second);
|
||||
return result;
|
||||
}
|
||||
} // namespace serif::tests::equations::gravity_port
|
||||
134
tests/equations/h1_divergence.cpp
Normal file
134
tests/equations/h1_divergence.cpp
Normal file
@@ -0,0 +1,134 @@
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
|
||||
#include "serif/tests/equations/gravity_port.hpp"
|
||||
|
||||
namespace {
|
||||
using namespace serif;
|
||||
using namespace serif::tests::equations;
|
||||
using gravity::GravityFixture;
|
||||
using gravity_port::project_vector;
|
||||
using Vector = ::mfem::Vector;
|
||||
using Displacement = blocks::displacement::vector;
|
||||
using Divergence = discretization::forms::Operand<Displacement, discretization::forms::Operation::Divergence>;
|
||||
|
||||
struct DivergenceInput {
|
||||
using Test = blocks::density::mass;
|
||||
using Operands = utils::types::TypeList<Divergence>;
|
||||
static constexpr std::string_view name = "H1 divergence input test";
|
||||
|
||||
serif::equations::PointLinearization<Operands> evaluate(const serif::equations::EquationPoint<Operands>& point) const {
|
||||
serif::equations::PointLinearization<Operands> result;
|
||||
result.residual = point.fields.get<Divergence>();
|
||||
result.field_derivatives.get<Divergence>() = 1.0;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int additional_order(Divergence, int) const {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
/// A smooth nonlinear displacement direction, independent of the field being differentiated.
|
||||
Vector direction_displacement(GravityFixture& fixture) {
|
||||
return project_vector(fixture.displacement_binding, [](const Vector& position, Vector& value) {
|
||||
const double x = position[0], y = position[1], z = position[2];
|
||||
value.SetSize(3);
|
||||
value[0] = 0.019 * x + 0.011 * y * z - 0.006 * z * z + 0.004 * 0.83 * y;
|
||||
value[1] = -0.016 * y + 0.008 * x * z + 0.005 * x * x - 0.003 * 0.83 * z;
|
||||
value[2] = 0.013 * z - 0.010 * x * y + 0.006 * y * y + 0.004 * 0.83 * x;
|
||||
});
|
||||
}
|
||||
|
||||
double relative(const Vector& actual, const Vector& expected) {
|
||||
Vector error(actual);
|
||||
error -= expected;
|
||||
|
||||
return error.Norml2() / std::max({actual.Norml2(), expected.Norml2(), 100.0 * std::numeric_limits<double>::epsilon()});
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("H1 divergence inputs retain component gradients under a nonsymmetric map", "[equations][h1_divergence][unit]") {
|
||||
for (const auto geometry : {::mfem::Element::TETRAHEDRON, ::mfem::Element::HEXAHEDRON}) {
|
||||
for (const auto ordering : {::mfem::Ordering::byNODES, ::mfem::Ordering::byVDIM}) {
|
||||
GravityFixture fixture(geometry, ordering);
|
||||
const auto value = project_vector(fixture.displacement_binding, [](const Vector& x, Vector& u) {
|
||||
u.SetSize(3);
|
||||
u[0] = 0.4 * x[0] - 0.2 * x[1] + 0.3 * x[2];
|
||||
u[1] = 0.1 * x[0] + 0.5 * x[1] - 0.1 * x[2];
|
||||
u[2] = -0.3 * x[0] + 0.2 * x[1] + 0.6 * x[2];
|
||||
});
|
||||
const auto geometry_value = project_vector(fixture.displacement_binding, [](const Vector& x, Vector& u) {
|
||||
u.SetSize(3);
|
||||
u[0] = 0.2 * x[0] + 0.07 * x[1];
|
||||
u[1] = -0.04 * x[0] - 0.1 * x[1] + 0.05 * x[2];
|
||||
u[2] = 0.03 * x[0] + 0.1 * x[2];
|
||||
});
|
||||
const auto direction = gravity_port::prepared_displacement(fixture, 0.83);
|
||||
const auto geometry_direction = direction_displacement(fixture);
|
||||
auto state = [&](const Vector& v) {
|
||||
return backend::make_term_coefficients(backend::bind(fixture.displacement_binding, v));
|
||||
};
|
||||
VolumeEquationFixture prepared{
|
||||
DivergenceInput{}, fixture.density_binding, fixture.displacement_binding,
|
||||
fixture.compactification, fixture.mapper, gravity::reference_policy()
|
||||
};
|
||||
prepared.prepare(state(value), geometry_value, fixture.workspace);
|
||||
Vector actual;
|
||||
prepared.residual(fixture.workspace, actual);
|
||||
|
||||
::mfem::DenseMatrix A(3), inverse(3);
|
||||
A = 0.0;
|
||||
A(0, 0) = 1.2;
|
||||
A(0, 1) = 0.07;
|
||||
A(1, 0) = -0.04;
|
||||
A(1, 1) = 0.9;
|
||||
A(1, 2) = 0.05;
|
||||
A(2, 0) = 0.03;
|
||||
A(2, 2) = 1.1;
|
||||
::mfem::CalcInverse(A, inverse);
|
||||
constexpr double gradient[3][3] = {{0.4, -0.2, 0.3}, {0.1, 0.5, -0.1}, {-0.3, 0.2, 0.6}};
|
||||
double divergence = 0.0;
|
||||
for (int component = 0; component < 3; ++component) {
|
||||
for (int derivative = 0; derivative < 3; ++derivative) {
|
||||
divergence += gradient[component][derivative] * inverse(derivative, component);
|
||||
}
|
||||
}
|
||||
|
||||
::mfem::ConstantCoefficient coefficient(divergence * A.Det());
|
||||
::mfem::LinearForm reference_load(&fixture.density_space);
|
||||
::mfem::Array<int> stellar{1, 1, 0};
|
||||
reference_load.AddDomainIntegrator(new ::mfem::DomainLFIntegrator(coefficient), stellar);
|
||||
reference_load.Assemble();
|
||||
CHECK(relative(actual, fixture.density_binding.gather(reference_load)) < 2.0e-12);
|
||||
|
||||
for (int column = 0; column < 3; ++column) {
|
||||
auto selected = direction, selected_geometry = geometry_direction;
|
||||
if (column == 0) {
|
||||
selected_geometry = 0.0;
|
||||
} else if (column == 1) {
|
||||
selected = 0.0;
|
||||
}
|
||||
|
||||
prepared.prepare(state(value), geometry_value, fixture.workspace);
|
||||
Vector derivative;
|
||||
prepared.apply_jacobian(state(selected), selected_geometry, fixture.workspace, derivative);
|
||||
auto plus = value, minus = value, plus_geometry = geometry_value, minus_geometry = geometry_value;
|
||||
constexpr double step = 1.0e-5;
|
||||
plus.Add(step, selected);
|
||||
minus.Add(-step, selected);
|
||||
plus_geometry.Add(step, selected_geometry);
|
||||
minus_geometry.Add(-step, selected_geometry);
|
||||
Vector plus_residual, minus_residual;
|
||||
prepared.prepare(state(plus), plus_geometry, fixture.workspace);
|
||||
prepared.residual(fixture.workspace, plus_residual);
|
||||
prepared.prepare(state(minus), minus_geometry, fixture.workspace);
|
||||
prepared.residual(fixture.workspace, minus_residual);
|
||||
plus_residual -= minus_residual;
|
||||
plus_residual *= 0.5 / step;
|
||||
CHECK(relative(derivative, plus_residual) < 3.0e-8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
109
tests/equations/h1_gradient.cpp
Normal file
109
tests/equations/h1_gradient.cpp
Normal file
@@ -0,0 +1,109 @@
|
||||
#include <cmath>
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include "mfem/fem/fe_coll.hpp"
|
||||
#include "mfem/mesh/mesh.hpp"
|
||||
#include "serif/tests/equations/test_equations.hpp"
|
||||
#include "serif/backend/mfem/operators/operand_evaluation.hpp"
|
||||
#include "serif/discretization/blocks/fields.hpp"
|
||||
|
||||
namespace {
|
||||
using Scalar = ::serif::tests::equations::Enthalpy;
|
||||
using Gradient = serif::discretization::forms::Operand<Scalar, serif::discretization::forms::Operation::Gradient>;
|
||||
using Evaluator = serif::operators::backend::mfem::OperandEvaluator<Gradient>;
|
||||
using serif::discretization::backend::mfem::BasisEvaluation;
|
||||
using serif::discretization::backend::mfem::BasisWorkspace;
|
||||
using serif::discretization::backend::mfem::ContractionMode;
|
||||
using serif::discretization::backend::mfem::TensorBasisCache;
|
||||
}
|
||||
|
||||
TEST_CASE("Scalar H1 gradients agree with physical MFEM bases and transpose actions", "[equations][scalar_gradient][unit]") {
|
||||
for (const auto geometry : {::mfem::Element::HEXAHEDRON, ::mfem::Element::TETRAHEDRON}) {
|
||||
auto mesh = ::mfem::Mesh::MakeCartesian3D(1, 1, 1, geometry, 1.7, 0.9, 1.2);
|
||||
// Shear as well as unequal lengths exposes inverse/transpose mistakes.
|
||||
for (int vertex = 0; vertex < mesh.GetNV(); ++vertex) {
|
||||
double* position = mesh.GetVertex(vertex);
|
||||
position[0] += 0.23 * position[1];
|
||||
position[2] -= 0.17 * position[1];
|
||||
}
|
||||
::mfem::H1_FECollection collection(3, 3);
|
||||
TensorBasisCache cache;
|
||||
BasisWorkspace workspace;
|
||||
for (int index = 0; index < mesh.GetNE(); ++index) {
|
||||
auto& transformation = *mesh.GetElementTransformation(index);
|
||||
const auto& element = *collection.FiniteElementForGeometry(mesh.GetElementBaseGeometry(index));
|
||||
const auto& rule = ::mfem::IntRules.Get(element.GetGeomType(), 8);
|
||||
for (const auto mode : {ContractionMode::automatic, ContractionMode::dense}) {
|
||||
BasisEvaluation basis;
|
||||
Evaluator::sample_basis(element, rule, basis, cache, mode, transformation, workspace);
|
||||
::mfem::Vector coefficients(element.GetDof()), values, dual(3 * rule.GetNPoints()), transpose;
|
||||
for (int dof = 0; dof < coefficients.Size(); ++dof) {
|
||||
coefficients[dof] = std::sin(0.37 * (dof + 1));
|
||||
}
|
||||
Evaluator::evaluate(basis, coefficients, values, workspace);
|
||||
::mfem::DenseMatrix physical_basis(element.GetDof(), 3);
|
||||
for (int point = 0; point < rule.GetNPoints(); ++point) {
|
||||
transformation.SetIntPoint(&rule.IntPoint(point));
|
||||
element.CalcPhysDShape(transformation, physical_basis);
|
||||
for (int axis = 0; axis < 3; ++axis) {
|
||||
double expected = 0.0;
|
||||
for (int dof = 0; dof < coefficients.Size(); ++dof) {
|
||||
expected += physical_basis(dof, axis) * coefficients[dof];
|
||||
}
|
||||
CHECK(std::abs(values[3 * point + axis] - expected) < 2.0e-11);
|
||||
dual[3 * point + axis] = std::cos(0.19 * (3 * point + axis + 1));
|
||||
}
|
||||
}
|
||||
Evaluator::integrate(basis, dual, transpose, workspace);
|
||||
CHECK(std::abs(values * dual - coefficients * transpose) < 2.0e-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Scalar H1 gradient geometry derivatives follow inverse transpose mapping", "[equations][scalar_gradient][unit]") {
|
||||
serif::mapping::MappingPointContext context;
|
||||
context.mapping_jacobian.SetSize(3);
|
||||
context.mapping_jacobian = 0.0;
|
||||
context.mapping_jacobian(0, 0) = 1.2;
|
||||
context.mapping_jacobian(1, 1) = 0.8;
|
||||
context.mapping_jacobian(2, 2) = 1.1;
|
||||
context.mapping_jacobian(0, 1) = 0.21;
|
||||
context.mapping_jacobian(2, 0) = -0.13;
|
||||
context.inverse_mapping_jacobian.SetSize(3);
|
||||
::mfem::CalcInverse(context.mapping_jacobian, context.inverse_mapping_jacobian);
|
||||
serif::mapping::MappingPointVariation variation;
|
||||
variation.mapping_jacobian_variation.SetSize(3);
|
||||
for (int row = 0; row < 3; ++row) {
|
||||
for (int column = 0; column < 3; ++column) {
|
||||
variation.mapping_jacobian_variation(row, column) = std::sin(0.41 * (3 * row + column + 1));
|
||||
}
|
||||
}
|
||||
::mfem::DenseMatrix product(3);
|
||||
variation.inverse_mapping_jacobian_variation.SetSize(3);
|
||||
::mfem::Mult(context.inverse_mapping_jacobian, variation.mapping_jacobian_variation, product);
|
||||
::mfem::Mult(product, context.inverse_mapping_jacobian, variation.inverse_mapping_jacobian_variation);
|
||||
variation.inverse_mapping_jacobian_variation *= -1.0;
|
||||
const Evaluator::ReferenceValue reference{0.7, -0.2, 0.4};
|
||||
const auto physical = Evaluator::physical_value(reference, context);
|
||||
// A^T grad_x = grad_X is independent of the implementation's inverse multiplication.
|
||||
for (int axis = 0; axis < 3; ++axis) {
|
||||
double recovered = 0.0;
|
||||
for (int component = 0; component < 3; ++component) {
|
||||
recovered += context.mapping_jacobian(component, axis) * physical[component];
|
||||
}
|
||||
CHECK(std::abs(recovered - reference[axis]) < 2.0e-14);
|
||||
}
|
||||
const auto derivative = Evaluator::geometry_variation(reference, context, variation);
|
||||
auto plus = context, minus = context;
|
||||
constexpr double step = 1.0e-6;
|
||||
plus.mapping_jacobian.Add(step, variation.mapping_jacobian_variation);
|
||||
minus.mapping_jacobian.Add(-step, variation.mapping_jacobian_variation);
|
||||
::mfem::CalcInverse(plus.mapping_jacobian, plus.inverse_mapping_jacobian);
|
||||
::mfem::CalcInverse(minus.mapping_jacobian, minus.inverse_mapping_jacobian);
|
||||
const auto plus_gradient = Evaluator::physical_value(reference, plus);
|
||||
const auto minus_gradient = Evaluator::physical_value(reference, minus);
|
||||
for (int axis = 0; axis < 3; ++axis) {
|
||||
CHECK(std::abs(derivative[axis] - (plus_gradient[axis] - minus_gradient[axis]) / (2.0 * step)) < 2.0e-10);
|
||||
}
|
||||
}
|
||||
86
tests/equations/h1_gradient_equation.cpp
Normal file
86
tests/equations/h1_gradient_equation.cpp
Normal file
@@ -0,0 +1,86 @@
|
||||
#include <cmath>
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include "serif/tests/equations/fixture.hpp"
|
||||
|
||||
namespace {
|
||||
using namespace serif::tests::equations;
|
||||
using Scalar = ::serif::tests::equations::Enthalpy;
|
||||
using Gradient = serif::discretization::forms::Operand<Scalar, serif::discretization::forms::Operation::Gradient>;
|
||||
|
||||
/** @brief Exercise a scalar diffusion row through the production volume assembly. */
|
||||
struct ScalarDiffusion : serif::equations::Contribution<Gradient> {
|
||||
using Operands = serif::utils::types::TypeList<Gradient>;
|
||||
static constexpr std::string_view name = "scalar gradient assembly regression";
|
||||
|
||||
/** @brief The flux is grad(z), so its partial with respect to grad(z) is identity. */
|
||||
serif::equations::PointLinearization<Operands, serif::discretization::forms::VectorValue>
|
||||
evaluate(const serif::equations::EquationPoint<Operands>& point) const {
|
||||
serif::equations::PointLinearization<Operands, serif::discretization::forms::VectorValue> result;
|
||||
result.residual = point.fields.get<Gradient>();
|
||||
for (int axis = 0; axis < 3; ++axis) {
|
||||
result.field_derivatives.get<Gradient>()[axis][axis] = 1.0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** @brief This linear flux introduces no extra polynomial coefficient. */
|
||||
static int additional_order(Gradient, int) { return 0; }
|
||||
};
|
||||
}
|
||||
|
||||
TEST_CASE("Scalar gradient test rows assemble diffusion and differentiate moving geometry", "[equations][scalar_gradient][unit]") {
|
||||
for (const auto geometry : {::mfem::Element::HEXAHEDRON, ::mfem::Element::TETRAHEDRON}) {
|
||||
Fixture fixture(geometry);
|
||||
const auto values = deterministic(fixture.enthalpy_binding.size(), 0.31);
|
||||
const auto direction = deterministic(values.Size(), 0.57);
|
||||
const auto displacement = fixture.displacement(0.6);
|
||||
const auto displacement_direction = fixture.displacement(0.2);
|
||||
const auto zero = constant(displacement.Size(), 0.0);
|
||||
const auto fields = [&](const ::mfem::Vector& coefficients) {
|
||||
return backend::make_term_coefficients(backend::bind(fixture.enthalpy_binding, coefficients));
|
||||
};
|
||||
serif::discretization::quadrature::RuleSet rules;
|
||||
rules.set_fallback_control({.fixed_order = 10});
|
||||
VolumeEquationFixture prepared{ScalarDiffusion{}, fixture.enthalpy_binding, fixture.displacement_binding,
|
||||
fixture.compactification, fixture.mapper, serif::discretization::quadrature::Policy{rules}};
|
||||
prepared.prepare(fields(values), zero, fixture.workspace);
|
||||
::mfem::Vector residual;
|
||||
prepared.residual(fixture.workspace, residual);
|
||||
::mfem::BilinearForm reference(&fixture.enthalpy_space);
|
||||
::mfem::Array<int> stellar{1, 1, 0};
|
||||
reference.AddDomainIntegrator(new ::mfem::DiffusionIntegrator(), stellar);
|
||||
reference.Assemble();
|
||||
reference.Finalize();
|
||||
const auto full_values = fixture.enthalpy_binding.scatter(values);
|
||||
::mfem::Vector full_residual(full_values.Size());
|
||||
reference.Mult(full_values, full_residual);
|
||||
CHECK(relative_error(residual, fixture.enthalpy_binding.gather(full_residual)) < 3.0e-12);
|
||||
|
||||
prepared.prepare(fields(values), displacement, fixture.workspace);
|
||||
if (geometry == ::mfem::Element::HEXAHEDRON) {
|
||||
CHECK(prepared.tensor_element_count() > 0);
|
||||
}
|
||||
::mfem::Vector action, transpose;
|
||||
prepared.apply_jacobian(fields(direction), displacement_direction, fixture.workspace, action);
|
||||
const auto dual = deterministic(residual.Size(), 0.83);
|
||||
prepared.apply_jacobian_transpose(dual, fixture.workspace, transpose);
|
||||
const auto packed_direction = packed(constant(0, 0.0), direction, displacement_direction);
|
||||
CHECK(std::abs(dual * action - packed_direction * transpose) < 3.0e-11);
|
||||
|
||||
constexpr double step = 1.0e-6;
|
||||
auto plus = values, minus = values, plus_displacement = displacement, minus_displacement = displacement;
|
||||
plus.Add(step, direction);
|
||||
minus.Add(-step, direction);
|
||||
plus_displacement.Add(step, displacement_direction);
|
||||
minus_displacement.Add(-step, displacement_direction);
|
||||
::mfem::Vector plus_residual, minus_residual;
|
||||
prepared.prepare(fields(plus), plus_displacement, fixture.workspace);
|
||||
prepared.residual(fixture.workspace, plus_residual);
|
||||
prepared.prepare(fields(minus), minus_displacement, fixture.workspace);
|
||||
prepared.residual(fixture.workspace, minus_residual);
|
||||
plus_residual -= minus_residual;
|
||||
plus_residual /= 2.0 * step;
|
||||
CHECK(relative_error(action, plus_residual) < 3.0e-8);
|
||||
}
|
||||
}
|
||||
118
tests/equations/h1_vector_gradient.cpp
Normal file
118
tests/equations/h1_vector_gradient.cpp
Normal file
@@ -0,0 +1,118 @@
|
||||
#include <cmath>
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include "mfem/fem/fe_coll.hpp"
|
||||
#include "mfem/mesh/mesh.hpp"
|
||||
#include "serif/backend/mfem/operators/operand_evaluation.hpp"
|
||||
#include "serif/discretization/blocks/fields.hpp"
|
||||
|
||||
namespace {
|
||||
namespace forms = serif::discretization::forms;
|
||||
namespace backend = serif::operators::backend::mfem;
|
||||
using Gradient = forms::Operand<serif::discretization::blocks::displacement::vector, forms::Operation::Gradient>;
|
||||
using Evaluator = backend::OperandEvaluator<Gradient>;
|
||||
}
|
||||
|
||||
TEST_CASE("Vector H1 gradients retain native physical components and transpose actions", "[equations][vector_gradient][unit]") {
|
||||
for (const auto geometry : {::mfem::Element::HEXAHEDRON, ::mfem::Element::TETRAHEDRON}) {
|
||||
auto mesh = ::mfem::Mesh::MakeCartesian3D(1, 1, 1, geometry, 1.7, 0.9, 1.2);
|
||||
for (int vertex = 0; vertex < mesh.GetNV(); ++vertex) {
|
||||
double* position = mesh.GetVertex(vertex);
|
||||
position[0] += 0.23 * position[1];
|
||||
position[2] -= 0.17 * position[1];
|
||||
}
|
||||
::mfem::H1_FECollection collection(3, 3);
|
||||
backend::TensorBasisCache cache;
|
||||
backend::BasisWorkspace workspace;
|
||||
for (int index = 0; index < mesh.GetNE(); ++index) {
|
||||
auto& transformation = *mesh.GetElementTransformation(index);
|
||||
const auto& element = *collection.FiniteElementForGeometry(mesh.GetElementBaseGeometry(index));
|
||||
const auto& rule = ::mfem::IntRules.Get(element.GetGeomType(), 8);
|
||||
for (const auto mode : {backend::ContractionMode::automatic, backend::ContractionMode::dense}) {
|
||||
backend::BasisEvaluation basis;
|
||||
Evaluator::sample_basis(element, rule, basis, cache, mode, transformation, workspace);
|
||||
CHECK(basis.uses_tensor() == (geometry == ::mfem::Element::HEXAHEDRON && mode == backend::ContractionMode::automatic));
|
||||
::mfem::Vector coefficients(3 * element.GetDof()), values, dual(9 * rule.GetNPoints()), transpose;
|
||||
for (int repeat = 0; repeat < 2; ++repeat) {
|
||||
for (int dof = 0; dof < coefficients.Size(); ++dof) {
|
||||
coefficients[dof] = repeat == 0 ? std::sin(0.37 * (dof + 1)) : 0.0;
|
||||
}
|
||||
Evaluator::evaluate(basis, coefficients, values, workspace);
|
||||
::mfem::DenseMatrix physical_basis(element.GetDof(), 3);
|
||||
for (int point = 0; point < rule.GetNPoints(); ++point) {
|
||||
transformation.SetIntPoint(&rule.IntPoint(point));
|
||||
element.CalcPhysDShape(transformation, physical_basis);
|
||||
for (int component = 0; component < 3; ++component) {
|
||||
for (int derivative = 0; derivative < 3; ++derivative) {
|
||||
double expected = 0.0;
|
||||
for (int dof = 0; dof < element.GetDof(); ++dof) {
|
||||
expected += physical_basis(dof, derivative) * coefficients[component * element.GetDof() + dof];
|
||||
}
|
||||
const int entry = 9 * point + 3 * component + derivative;
|
||||
CHECK(std::abs(values[entry] - expected) < 2.0e-11);
|
||||
dual[entry] = repeat == 0 ? std::cos(0.19 * (entry + 1)) : 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
Evaluator::integrate(basis, dual, transpose, workspace);
|
||||
CHECK(std::abs(values * dual - coefficients * transpose) < 2.0e-10);
|
||||
if (repeat != 0) {
|
||||
CHECK(values.Norml2() == 0.0);
|
||||
CHECK(transpose.Norml2() == 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Vector gradient mapping differentiates right multiplication by the inverse map", "[equations][vector_gradient][unit]") {
|
||||
serif::mapping::MappingPointContext context;
|
||||
context.mapping_jacobian.SetSize(3);
|
||||
context.mapping_jacobian = 0.0;
|
||||
context.mapping_jacobian(0, 0) = 1.2;
|
||||
context.mapping_jacobian(1, 1) = 0.8;
|
||||
context.mapping_jacobian(2, 2) = 1.1;
|
||||
context.mapping_jacobian(0, 1) = 0.21;
|
||||
context.mapping_jacobian(2, 0) = -0.13;
|
||||
context.inverse_mapping_jacobian.SetSize(3);
|
||||
::mfem::CalcInverse(context.mapping_jacobian, context.inverse_mapping_jacobian);
|
||||
serif::mapping::MappingPointVariation variation;
|
||||
variation.mapping_jacobian_variation.SetSize(3);
|
||||
for (int row = 0; row < 3; ++row) {
|
||||
for (int column = 0; column < 3; ++column) {
|
||||
variation.mapping_jacobian_variation(row, column) = std::sin(0.41 * (3 * row + column + 1));
|
||||
}
|
||||
}
|
||||
::mfem::DenseMatrix product(3);
|
||||
variation.inverse_mapping_jacobian_variation.SetSize(3);
|
||||
::mfem::Mult(context.inverse_mapping_jacobian, variation.mapping_jacobian_variation, product);
|
||||
::mfem::Mult(product, context.inverse_mapping_jacobian, variation.inverse_mapping_jacobian_variation);
|
||||
variation.inverse_mapping_jacobian_variation *= -1.0;
|
||||
const Evaluator::ReferenceValue reference{0.7, -0.2, 0.4, -0.6, 0.3, 0.1, 0.8, -0.9, 0.5};
|
||||
const auto physical = Evaluator::physical_value(reference, context);
|
||||
const auto derivative = Evaluator::geometry_variation(reference, context, variation);
|
||||
for (int component = 0; component < 3; ++component) {
|
||||
for (int axis = 0; axis < 3; ++axis) {
|
||||
double recovered = 0.0;
|
||||
for (int inner = 0; inner < 3; ++inner) {
|
||||
recovered += physical[component][inner] * context.mapping_jacobian(inner, axis);
|
||||
}
|
||||
CHECK(std::abs(recovered - reference[3 * component + axis]) < 2.0e-14);
|
||||
}
|
||||
}
|
||||
for (const double step : {1.0e-5, 1.0e-6}) {
|
||||
auto plus = context, minus = context;
|
||||
plus.mapping_jacobian.Add(step, variation.mapping_jacobian_variation);
|
||||
minus.mapping_jacobian.Add(-step, variation.mapping_jacobian_variation);
|
||||
::mfem::CalcInverse(plus.mapping_jacobian, plus.inverse_mapping_jacobian);
|
||||
::mfem::CalcInverse(minus.mapping_jacobian, minus.inverse_mapping_jacobian);
|
||||
const auto plus_gradient = Evaluator::physical_value(reference, plus);
|
||||
const auto minus_gradient = Evaluator::physical_value(reference, minus);
|
||||
for (int component = 0; component < 3; ++component) {
|
||||
for (int axis = 0; axis < 3; ++axis) {
|
||||
CHECK(std::abs(derivative[component][axis] - (plus_gradient[component][axis] - minus_gradient[component][axis]) / (2.0 * step)) < 3.0e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
196
tests/equations/h1_vector_gradient_equation.cpp
Normal file
196
tests/equations/h1_vector_gradient_equation.cpp
Normal file
@@ -0,0 +1,196 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include "mfem/fem/bilinearform.hpp"
|
||||
#include "mfem/fem/bilininteg.hpp"
|
||||
#include "serif/tests/equations/fixture.hpp"
|
||||
#include "serif/tests/nonconforming.hpp"
|
||||
|
||||
namespace {
|
||||
using namespace serif::tests::equations;
|
||||
namespace forms = serif::discretization::forms;
|
||||
using Vector = ::mfem::Vector;
|
||||
using VectorField = blocks::displacement::vector;
|
||||
using Value = forms::Operand<VectorField>;
|
||||
using Gradient = forms::Operand<VectorField, forms::Operation::Gradient>;
|
||||
using StellarDomains = serif::discretization::domain::StellarDomains;
|
||||
|
||||
/** @brief Supply the componentwise diffusion flux for comparison with native MFEM. */
|
||||
struct VectorDiffusion : serif::equations::Contribution<Gradient, StellarDomains> {
|
||||
using Operands = serif::utils::types::TypeList<Gradient>;
|
||||
static constexpr std::string_view name = "vector diffusion regression";
|
||||
/** @brief Each physical gradient component contributes its own identity derivative. */
|
||||
serif::equations::PointLinearization<Operands, forms::MatrixValue> evaluate(const serif::equations::EquationPoint<Operands>& point) const {
|
||||
serif::equations::PointLinearization<Operands, forms::MatrixValue> result;
|
||||
result.residual = point.fields.get<Gradient>();
|
||||
for (int entry = 0; entry < 9; ++entry) {
|
||||
result.field_derivatives.get<Gradient>()[entry][entry] = 1.0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/** @brief The diffusion flux has no additional polynomial coefficient. */
|
||||
static int additional_order(Gradient, int) { return 0; }
|
||||
};
|
||||
|
||||
/** @brief Couple transposed gradient entries and vector values to expose tensor indexing errors. */
|
||||
struct CoupledFlux : serif::equations::Contribution<Gradient, StellarDomains> {
|
||||
using Operands = serif::utils::types::TypeList<Gradient, Value>;
|
||||
static constexpr std::string_view name = "coupled tensor flux regression";
|
||||
/** @brief Differentiate a nonlinear manufactured flux, including its explicit position dependence. */
|
||||
serif::equations::PointLinearization<Operands, forms::MatrixValue> evaluate(const serif::equations::EquationPoint<Operands>& point) const {
|
||||
const auto& gradient = point.fields.get<Gradient>();
|
||||
const auto& value = point.fields.get<Value>();
|
||||
serif::equations::PointLinearization<Operands, forms::MatrixValue> result;
|
||||
for (int component = 0; component < 3; ++component) {
|
||||
for (int axis = 0; axis < 3; ++axis) {
|
||||
const int entry = 3 * component + axis;
|
||||
result.residual[component][axis] = gradient[component][axis] + 0.37 * gradient[axis][component] +
|
||||
0.11 * gradient[component][axis] * gradient[component][axis] + 0.19 * value[component] * value[axis] +
|
||||
0.07 * point.position[axis] * value[component];
|
||||
auto& gradient_partial = result.field_derivatives.get<Gradient>();
|
||||
gradient_partial[entry][entry] += 1.0 + 0.22 * gradient[component][axis];
|
||||
gradient_partial[entry][3 * axis + component] += 0.37;
|
||||
auto& value_partial = result.field_derivatives.get<Value>();
|
||||
value_partial[entry][component] += 0.19 * value[axis] + 0.07 * point.position[axis];
|
||||
value_partial[entry][axis] += 0.19 * value[component];
|
||||
result.position_derivative[entry][axis] = 0.07 * value[component];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/** @brief Account for the quadratic factors in this manufactured flux. */
|
||||
template <typename Operand> static int additional_order(Operand, int order) { return order + 1; }
|
||||
};
|
||||
|
||||
/** @brief Exercise the reverse rectangular derivative from tensor input to vector output. */
|
||||
struct CoupledForce : serif::equations::Contribution<Value, StellarDomains> {
|
||||
using Operands = serif::utils::types::TypeList<Gradient, Value>;
|
||||
static constexpr std::string_view name = "coupled vector force regression";
|
||||
/** @brief Contract the gradient with velocity and differentiate both independent operands. */
|
||||
serif::equations::PointLinearization<Operands, forms::VectorValue> evaluate(const serif::equations::EquationPoint<Operands>& point) const {
|
||||
const auto& gradient = point.fields.get<Gradient>();
|
||||
const auto& value = point.fields.get<Value>();
|
||||
serif::equations::PointLinearization<Operands, forms::VectorValue> result;
|
||||
for (int component = 0; component < 3; ++component) {
|
||||
for (int axis = 0; axis < 3; ++axis) {
|
||||
result.residual[component] += gradient[component][axis] * value[axis];
|
||||
result.field_derivatives.get<Gradient>()[component][3 * component + axis] = value[axis];
|
||||
result.field_derivatives.get<Value>()[component][axis] = gradient[component][axis];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/** @brief The convection-like product adds one field's polynomial order. */
|
||||
template <typename Operand> static int additional_order(Operand, int order) { return order; }
|
||||
};
|
||||
|
||||
/** @brief Publish the flux and force as two contributions of one vector row. */
|
||||
struct CoupledEquation {
|
||||
using Test = VectorField;
|
||||
static constexpr std::string_view name = "coupled tensor flux and vector force regression";
|
||||
[[nodiscard]] auto contributions() const { return std::tuple{CoupledFlux{}, CoupledForce{}}; }
|
||||
};
|
||||
|
||||
/** @brief Fix identical quadrature for native MFEM and the prepared equation. */
|
||||
serif::discretization::quadrature::Policy integration_policy() {
|
||||
serif::discretization::quadrature::RuleSet rules;
|
||||
rules.set_fallback_control({.fixed_order = 12});
|
||||
return serif::discretization::quadrature::Policy{rules};
|
||||
}
|
||||
|
||||
/** @brief Give the physical vector field a coefficient input independent of mesh displacement. */
|
||||
auto field(const Fixture& fixture, const Vector& values) {
|
||||
return backend::make_term_coefficients(backend::bind(fixture.displacement_binding, values));
|
||||
}
|
||||
|
||||
/** @brief Compare native local assembly followed by P transpose with the reduced backend residual. */
|
||||
void check_native_diffusion(Fixture& fixture, backend::ContractionMode mode) {
|
||||
const Vector values = deterministic(fixture.displacement_binding.size(), 0.31);
|
||||
const Vector zero = constant(values.Size(), 0.0);
|
||||
VolumeEquationFixture prepared{VectorDiffusion{}, fixture.displacement_binding, fixture.displacement_binding,
|
||||
fixture.compactification, fixture.mapper, integration_policy(), mode};
|
||||
prepared.prepare(field(fixture, values), zero, fixture.workspace);
|
||||
Vector actual;
|
||||
prepared.residual(fixture.workspace, actual);
|
||||
::mfem::BilinearForm reference(&fixture.displacement_space);
|
||||
auto* integrator = new ::mfem::VectorDiffusionIntegrator();
|
||||
integrator->SetIntRule(&::mfem::IntRules.Get(fixture.mesh.GetElementBaseGeometry(0), 12));
|
||||
::mfem::Array<int> stellar{1, 1, 0};
|
||||
reference.AddDomainIntegrator(integrator, stellar);
|
||||
reference.Assemble();
|
||||
reference.Finalize();
|
||||
const Vector full_values = fixture.displacement_binding.scatter(values);
|
||||
Vector native(full_values.Size());
|
||||
reference.Mult(full_values, native);
|
||||
CHECK(relative_error(actual, fixture.displacement_binding.accumulate_dual(native)) < 2.0e-11);
|
||||
if (fixture.mesh.GetElementBaseGeometry(0) == ::mfem::Geometry::CUBE) {
|
||||
CHECK((prepared.tensor_element_count() > 0) == (mode == backend::ContractionMode::automatic));
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Check each independent field/geometry column, combined action, and complete transpose. */
|
||||
void check_coupled_derivatives(Fixture& fixture, backend::ContractionMode mode) {
|
||||
const Vector values = deterministic(fixture.displacement_binding.size(), 0.31);
|
||||
const Vector direction = deterministic(values.Size(), 0.57);
|
||||
const Vector displacement = fixture.displacement(0.6);
|
||||
const Vector displacement_direction = fixture.displacement(0.2);
|
||||
const CoupledEquation equation;
|
||||
VolumeEquationFixture prepared{equation, fixture.displacement_binding, fixture.displacement_binding,
|
||||
fixture.compactification, fixture.mapper, integration_policy(), mode};
|
||||
const Vector dual = deterministic(fixture.displacement_binding.size(), 0.83);
|
||||
for (int column = 0; column < 3; ++column) {
|
||||
Vector selected = direction, selected_geometry = displacement_direction;
|
||||
if (column == 0) { selected_geometry = 0.0; }
|
||||
if (column == 1) { selected = 0.0; }
|
||||
prepared.prepare(field(fixture, values), displacement, fixture.workspace);
|
||||
Vector action, transpose;
|
||||
prepared.apply_jacobian(field(fixture, selected), selected_geometry, fixture.workspace, action);
|
||||
prepared.apply_jacobian_transpose(dual, fixture.workspace, transpose);
|
||||
const Vector packed_direction = packed(constant(0, 0.0), selected, selected_geometry);
|
||||
const double forward = dual * action;
|
||||
CHECK(std::abs(forward - packed_direction * transpose) < 3.0e-11 * std::max(1.0, std::abs(forward)));
|
||||
// Geometry directions are small; these steps avoid subtractive cancellation without relaxing the tolerance.
|
||||
for (const double step : {1.0e-4, 1.0e-5}) {
|
||||
CAPTURE(column, step);
|
||||
Vector plus = values, minus = values, plus_geometry = displacement, minus_geometry = displacement;
|
||||
plus.Add(step, selected);
|
||||
minus.Add(-step, selected);
|
||||
plus_geometry.Add(step, selected_geometry);
|
||||
minus_geometry.Add(-step, selected_geometry);
|
||||
Vector plus_residual, minus_residual;
|
||||
prepared.prepare(field(fixture, plus), plus_geometry, fixture.workspace);
|
||||
prepared.residual(fixture.workspace, plus_residual);
|
||||
prepared.prepare(field(fixture, minus), minus_geometry, fixture.workspace);
|
||||
prepared.residual(fixture.workspace, minus_residual);
|
||||
plus_residual -= minus_residual;
|
||||
plus_residual /= 2.0 * step;
|
||||
CHECK(relative_error(action, plus_residual) < 5.0e-8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Vector gradient equations retain dense tensor and ordering equivalence", "[equations][vector_gradient][unit]") {
|
||||
for (const auto geometry : {::mfem::Element::HEXAHEDRON, ::mfem::Element::TETRAHEDRON}) {
|
||||
for (const auto ordering : {::mfem::Ordering::byNODES, ::mfem::Ordering::byVDIM}) {
|
||||
Fixture fixture(geometry, ordering);
|
||||
for (const auto mode : {backend::ContractionMode::automatic, backend::ContractionMode::dense}) {
|
||||
CAPTURE(geometry, ordering, mode);
|
||||
check_native_diffusion(fixture, mode);
|
||||
check_coupled_derivatives(fixture, mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Vector gradient equations preserve true-coordinate work on STROID hanging meshes", "[equations][vector_gradient][integration][nonconforming]") {
|
||||
for (const auto ordering : {::mfem::Ordering::byNODES, ::mfem::Ordering::byVDIM}) {
|
||||
auto generated = serif::tests::nonconforming::mesh(0, 3);
|
||||
REQUIRE(generated.mesh->Nonconforming());
|
||||
Fixture fixture(::mfem::Mesh(*generated.mesh), ordering);
|
||||
REQUIRE(fixture.displacement_space.GetTrueVSize() < fixture.displacement_space.GetVSize());
|
||||
CAPTURE(ordering);
|
||||
check_native_diffusion(fixture, backend::ContractionMode::automatic);
|
||||
check_coupled_derivatives(fixture, backend::ContractionMode::automatic);
|
||||
}
|
||||
}
|
||||
109
tests/equations/manual_closure.cpp
Normal file
109
tests/equations/manual_closure.cpp
Normal file
@@ -0,0 +1,109 @@
|
||||
#include "serif/tests/equations/fixture.hpp"
|
||||
#include "serif/tests/mapping/volume_variation.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace serif::tests::equations {
|
||||
::mfem::Vector manual_closure(
|
||||
Fixture& f,
|
||||
const eos::models::Polytrope& eos,
|
||||
const ::mfem::Vector& density,
|
||||
const ::mfem::Vector& enthalpy,
|
||||
const ::mfem::Vector& displacement,
|
||||
const ::mfem::Vector* density_direction,
|
||||
const ::mfem::Vector* enthalpy_direction,
|
||||
const ::mfem::Vector* displacement_direction
|
||||
) {
|
||||
::mfem::GridFunction rho(&f.density_space), h(&f.enthalpy_space), u(&f.displacement_space);
|
||||
::mfem::GridFunction drho(&f.density_space), dh(&f.enthalpy_space), du(&f.displacement_space);
|
||||
rho = f.density_binding.scatter(density);
|
||||
h = f.enthalpy_binding.scatter(enthalpy);
|
||||
u = f.displacement_binding.scatter(displacement);
|
||||
drho = 0.0;
|
||||
dh = 0.0;
|
||||
du = 0.0;
|
||||
if (density_direction != nullptr) {
|
||||
drho = f.density_binding.scatter(*density_direction);
|
||||
}
|
||||
if (enthalpy_direction != nullptr) {
|
||||
dh = f.enthalpy_binding.scatter(*enthalpy_direction);
|
||||
}
|
||||
if (displacement_direction != nullptr) {
|
||||
du = f.displacement_binding.scatter(*displacement_direction);
|
||||
}
|
||||
const bool action =
|
||||
density_direction != nullptr || enthalpy_direction != nullptr || displacement_direction != nullptr;
|
||||
const double n = eos.polytropic_index();
|
||||
const double scale = (n + 1.0) * eos.polytropic_constant();
|
||||
::mfem::Vector result(f.density_space.GetVSize());
|
||||
result = 0.0;
|
||||
mapping::DMWorkspace workspace;
|
||||
for (int element = 0; element < f.mesh.GetNE(); ++element) {
|
||||
if (!Schema::domain_id_belongs_to<discretization::domain::StellarDomains>(f.mesh.GetAttribute(element))) {
|
||||
continue;
|
||||
}
|
||||
auto& transformation = *f.mesh.GetElementTransformation(element);
|
||||
const auto& test = *f.density_space.GetFE(element);
|
||||
const auto& displacement_element = *f.displacement_space.GetFE(element);
|
||||
const int order = std::max(
|
||||
2 * test.GetOrder(), test.GetOrder() + f.enthalpy_space.GetFE(element)->GetOrder() +
|
||||
static_cast<int>(std::ceil(std::max(0.0, n - 1.0) * 3))
|
||||
) +
|
||||
transformation.OrderW() + 3 * (displacement_element.GetOrder() - 1);
|
||||
const auto& rule = ::mfem::IntRules.Get(transformation.GetGeometryType(), order);
|
||||
::mfem::Array<int> dofs;
|
||||
auto* dof_transformation = f.displacement_space.GetElementVDofs(element, dofs);
|
||||
::mfem::Vector u_dofs, du_dofs;
|
||||
u.GetSubVector(dofs, u_dofs);
|
||||
du.GetSubVector(dofs, du_dofs);
|
||||
if (dof_transformation != nullptr) {
|
||||
dof_transformation->InvTransformPrimal(u_dofs);
|
||||
dof_transformation->InvTransformPrimal(du_dofs);
|
||||
}
|
||||
const auto u_data = mapping::element_displacement_data_from_element_vdofs(displacement_element, u_dofs);
|
||||
const auto du_data = mapping::element_displacement_data_from_element_vdofs(displacement_element, du_dofs);
|
||||
::mfem::Vector compactification_dofs(f.enthalpy_space.GetFE(element)->GetDof());
|
||||
compactification_dofs = 0.0;
|
||||
const mapping::ElementCompactificationData compactification(
|
||||
*f.enthalpy_space.GetFE(element), compactification_dofs
|
||||
);
|
||||
const mapping::ElementMappingData mapping_data{u_data, compactification};
|
||||
::mfem::Vector shape(test.GetDof()), local(test.GetDof());
|
||||
local = 0.0;
|
||||
for (int point = 0; point < rule.GetNPoints(); ++point) {
|
||||
const auto& ip = rule.IntPoint(point);
|
||||
const double enthalpy_value = h.GetValue(element, ip);
|
||||
const double eos_density = std::pow(enthalpy_value / scale, n);
|
||||
const double eos_derivative = n / scale * std::pow(enthalpy_value / scale, n - 1.0);
|
||||
const double closure = rho.GetValue(element, ip) - eos_density;
|
||||
const double variation = drho.GetValue(element, ip) - eos_derivative * dh.GetValue(element, ip);
|
||||
mapping::VolumeMappingContext context;
|
||||
const auto status = f.mapper.evaluate_volume(mapping_data, transformation, ip, workspace, context);
|
||||
utils::errors::serif_ensure<utils::errors::SERiFErrorCode::INVALID_MAPPING>(
|
||||
status == mapping::MappingStatus::VALID, "Invalid manual-reference geometry."
|
||||
);
|
||||
double integrand = context.quadrature.physical_integration_weight * closure;
|
||||
if (action) {
|
||||
mapping::VolumeMappingVariation geometry_variation;
|
||||
const auto variation_status = serif::tests::evaluate_volume_variation(
|
||||
f.mapper, mapping_data, du_data, transformation, ip, context, workspace, geometry_variation
|
||||
);
|
||||
utils::errors::serif_ensure<utils::errors::SERiFErrorCode::INVALID_MAPPING>(
|
||||
variation_status == mapping::MappingStatus::VALID, "Invalid manual-reference variation."
|
||||
);
|
||||
integrand = context.quadrature.physical_integration_weight * variation +
|
||||
geometry_variation.weight_variation * closure;
|
||||
}
|
||||
test.CalcShape(ip, shape);
|
||||
local.Add(integrand, shape);
|
||||
}
|
||||
auto* test_transformation = f.density_space.GetElementVDofs(element, dofs);
|
||||
if (test_transformation != nullptr) {
|
||||
test_transformation->TransformDual(local);
|
||||
}
|
||||
result.AddElementVector(dofs, local);
|
||||
}
|
||||
return f.density_binding.gather(result);
|
||||
}
|
||||
} // namespace serif::tests::equations
|
||||
245
tests/equations/mass_normalization.cpp
Normal file
245
tests/equations/mass_normalization.cpp
Normal file
@@ -0,0 +1,245 @@
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
#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::MassNormalization;
|
||||
using Prepared = VolumeEquationFixture<Equation, Schema>;
|
||||
using Vector = ::mfem::Vector;
|
||||
|
||||
auto state(GravityFixture& f, const Vector& density) {
|
||||
return backend::make_term_coefficients(backend::bind(f.density_binding, density));
|
||||
}
|
||||
|
||||
auto make_operator(GravityFixture& f, double target) {
|
||||
return Prepared{Equation{target}, f.constant_binding, f.displacement_binding, f.compactification, f.mapper, reference_policy()};
|
||||
}
|
||||
|
||||
double residual(GravityFixture& f, Prepared& op, const Vector& density, const Vector& displacement) {
|
||||
op.prepare(state(f, density), displacement, f.workspace);
|
||||
Vector result;
|
||||
op.residual(f.workspace, result);
|
||||
REQUIRE(result.Size() == 1);
|
||||
return result[0];
|
||||
}
|
||||
|
||||
double action(GravityFixture& f, Prepared& op, const Vector& density, const Vector& displacement) {
|
||||
Vector result;
|
||||
op.apply_jacobian(state(f, density), displacement, f.workspace, result);
|
||||
REQUIRE(result.Size() == 1);
|
||||
return result[0];
|
||||
}
|
||||
|
||||
// MeanField prepared_mass_normalization.cpp:66,98,132. Preserve the original profiles.
|
||||
Vector density(GravityFixture& f, double phase, bool direction = false) {
|
||||
return f.project(f.density_binding, [=](const Vector& x) {
|
||||
if (direction) {
|
||||
return 0.19 * std::sin(0.71 * x[0] + phase) - 0.13 * std::cos(0.89 * x[1] - phase) + 0.08 * x[2];
|
||||
}
|
||||
return 0.91 + 0.07 * std::sin(0.83 * x[0] + phase) + 0.05 * std::cos(0.61 * 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(expected), 100.0 * std::numeric_limits<double>::epsilon());
|
||||
}
|
||||
|
||||
// Independent MFEM quadrature of rho det(A) and delta-rho det(A) + rho cof(A):delta-A.
|
||||
double reference(GravityFixture& f, const Vector& rho, const Vector& u, const Vector* drho = nullptr, const Vector* du = nullptr) {
|
||||
::mfem::GridFunction density(&f.density_space), displacement(&f.displacement_space);
|
||||
::mfem::GridFunction delta_density(&f.density_space), delta_displacement(&f.displacement_space);
|
||||
density = f.density_binding.scatter(rho);
|
||||
displacement = f.displacement_binding.scatter(u);
|
||||
if (drho) { delta_density = f.density_binding.scatter(*drho); }
|
||||
if (du) { delta_displacement = f.displacement_binding.scatter(*du); }
|
||||
double result = 0.0;
|
||||
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);
|
||||
::mfem::DenseMatrix A(3), dA(3), inverse(3);
|
||||
displacement.GetVectorGradient(transform, A);
|
||||
for (int axis = 0; axis < 3; ++axis) { A(axis, axis) += 1.0; }
|
||||
double integrand = density.GetValue(transform, ip);
|
||||
if (drho || du) {
|
||||
integrand = drho ? delta_density.GetValue(transform, ip) : 0.0;
|
||||
if (du) {
|
||||
delta_displacement.GetVectorGradient(transform, dA);
|
||||
::mfem::CalcInverse(A, inverse);
|
||||
double trace = 0.0;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
for (int j = 0; j < 3; ++j) { trace += inverse(i, j) * dA(j, i); }
|
||||
}
|
||||
integrand += density.GetValue(transform, ip) * trace;
|
||||
}
|
||||
}
|
||||
result += ip.weight * transform.Weight() * A.Det() * integrand;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void check_columns(GravityFixture& f) {
|
||||
CAPTURE(f.mesh.GetNE(), f.mesh.GetElementGeometry(0), f.displacement_space.GetOrdering());
|
||||
f.set_compactification();
|
||||
const auto zero_rho = constant(f.density_binding.size(), 0.0), zero_u = constant(f.displacement_binding.size(), 0.0);
|
||||
// Original density and geometry finite differences, with their original steps and tolerances.
|
||||
for (int column = 0; column < 2; ++column) {
|
||||
const auto rho = density(f, column == 0 ? 0.31 : 0.37);
|
||||
const auto u = displacement(f, column == 0 ? 0.43 : 0.51);
|
||||
const auto drho = column == 0 ? density(f, 0.67, true) : zero_rho;
|
||||
const auto du = column == 0 ? zero_u : displacement(f, -0.79);
|
||||
auto op = make_operator(f, column == 0 ? 1.23 : 1.11);
|
||||
(void)residual(f, op, rho, u);
|
||||
const double actual = action(f, op, drho, du);
|
||||
const double step = column == 0 ? 1.0e-3 : 1.0e-6;
|
||||
auto plus_rho = rho, minus_rho = rho, plus_u = u, minus_u = u;
|
||||
plus_rho.Add(step, drho); minus_rho.Add(-step, drho);
|
||||
plus_u.Add(step, du); minus_u.Add(-step, du);
|
||||
const double difference = (residual(f, op, plus_rho, plus_u) - residual(f, op, minus_rho, minus_u)) / (2.0 * step);
|
||||
CAPTURE(column, actual, difference);
|
||||
CHECK(relative(actual, difference) < (column == 0 ? 3.0e-8 : 2.0e-7));
|
||||
}
|
||||
|
||||
// Original complete action, independent reference, column sum, and supported local adapter routing.
|
||||
const auto rho = density(f, 0.47), u = displacement(f, 0.57);
|
||||
const auto drho = density(f, 0.71, true), du = displacement(f, -0.63);
|
||||
auto op = make_operator(f, 1.19);
|
||||
const auto initial = residual(f, op, rho, u);
|
||||
CHECK(relative(initial, reference(f, rho, u) - 1.19) < 1.0e-12);
|
||||
const double complete = action(f, op, drho, du);
|
||||
CHECK(relative(complete, reference(f, rho, u, &drho, &du)) < 2.0e-11);
|
||||
CHECK(relative(complete, action(f, op, drho, zero_u) + action(f, op, zero_rho, du)) < 2.0e-15);
|
||||
backend::JacobianOperator adapter(op.cache());
|
||||
CHECK(adapter.Height() == 1);
|
||||
CHECK(adapter.Width() == drho.Size() + du.Size());
|
||||
Vector joined(adapter.Width()), result;
|
||||
std::copy_n(drho.GetData(), drho.Size(), joined.GetData());
|
||||
std::copy_n(du.GetData(), du.Size(), joined.GetData() + drho.Size());
|
||||
adapter.Mult(joined, result);
|
||||
CHECK(result[0] == complete);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Mass normalization retains affine volume scaling and target subtraction", "[equations][mass_normalization][integration]") {
|
||||
auto fixture = gravity_port::stellar_fixture();
|
||||
auto& f = *fixture;
|
||||
const auto rho = constant(f.density_binding.size(), 1.37), zero = constant(f.displacement_binding.size(), 0.0);
|
||||
auto op = make_operator(f, 0.73);
|
||||
const double undeformed = residual(f, op, rho, zero) + 0.73;
|
||||
CHECK(relative(undeformed, reference(f, rho, zero)) < 1.0e-12);
|
||||
CHECK(relative(residual(f, op, rho, zero), undeformed - 0.73) < 2.0e-15);
|
||||
const double deformed = residual(f, op, rho, gravity_port::affine_displacement(f, 0.086)) + 0.73;
|
||||
CHECK(relative(deformed / undeformed, std::pow(1.0 + 0.086, 3)) < 5.0e-7);
|
||||
}
|
||||
|
||||
TEST_CASE("Mass normalization retains density geometry and complete action accuracy", "[equations][mass_normalization][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);
|
||||
check_columns(f);
|
||||
}
|
||||
}
|
||||
auto fixture = gravity_port::stellar_fixture();
|
||||
check_columns(*fixture);
|
||||
}
|
||||
|
||||
TEST_CASE("Global mass preparations own their density state", "[equations][mass_normalization][unit]") {
|
||||
GravityFixture f;
|
||||
auto rho = density(f, 0.47);
|
||||
const auto u = displacement(f, 0.57);
|
||||
VolumeEquationFixture op{Equation{0.73}, f.constant_binding, f.displacement_binding, f.compactification, f.mapper, reference_policy()};
|
||||
const double expected = reference(f, rho, u) - 0.73;
|
||||
op.prepare(state(f, rho), u, f.workspace);
|
||||
Vector result;
|
||||
op.residual(f.workspace, result);
|
||||
CHECK(relative(result[0], expected) < 2.0e-15);
|
||||
rho *= 1.2;
|
||||
op.residual(f.workspace, result);
|
||||
CHECK(relative(result[0], expected) < 2.0e-15);
|
||||
op.prepare(state(f, rho), u, f.workspace);
|
||||
op.residual(f.workspace, result);
|
||||
CHECK(relative(result[0], reference(f, rho, u) - 0.73) < 2.0e-15);
|
||||
const auto drho = density(f, 0.71, true), du = displacement(f, -0.63);
|
||||
op.apply_jacobian(state(f, drho), du, f.workspace, result);
|
||||
CHECK(relative(result[0], reference(f, rho, u, &drho, &du)) < 2.0e-11);
|
||||
}
|
||||
|
||||
TEST_CASE("Mass normalization rejects invalid targets density and geometry then recovers", "[equations][mass_normalization][unit]") {
|
||||
for (double target : {0.0, -1.0, std::numeric_limits<double>::infinity(), std::numeric_limits<double>::quiet_NaN()}) {
|
||||
CHECK_THROWS_AS(Equation{target}, std::invalid_argument);
|
||||
}
|
||||
GravityFixture f;
|
||||
auto op = make_operator(f, 1.0);
|
||||
auto rho = constant(f.density_binding.size(), 1.0);
|
||||
auto u = constant(f.displacement_binding.size(), 0.0);
|
||||
const double expected = residual(f, op, rho, u);
|
||||
rho = std::numeric_limits<double>::quiet_NaN();
|
||||
auto rejected = op.try_prepare(state(f, rho), u, 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, rho), u, f.workspace), std::domain_error);
|
||||
rho = 1.0;
|
||||
u = gravity_port::affine_displacement(f, -2.0);
|
||||
rejected = op.try_prepare(state(f, rho), u, f.workspace);
|
||||
REQUIRE_FALSE(rejected.has_value());
|
||||
CHECK(rejected.error().reason == backend::PreparationRejectionReason::MAPPING);
|
||||
u = 0.0;
|
||||
CHECK(residual(f, op, rho, u) == expected);
|
||||
}
|
||||
|
||||
// MeanField prepared_mass_normalization.cpp:282: finite coefficients can overflow during interpolation.
|
||||
TEST_CASE("Mass normalization rejects nonfinite density interpolation and recovers", "[equations][mass_normalization][unit]") {
|
||||
auto fixture = gravity_port::stellar_fixture();
|
||||
auto& f = *fixture;
|
||||
auto rho = constant(f.density_binding.size(), -0.5 * std::numeric_limits<double>::max());
|
||||
const auto u = constant(f.displacement_binding.size(), 0.0);
|
||||
auto op = make_operator(f, std::numeric_limits<double>::max());
|
||||
const auto rejected = op.try_prepare(state(f, rho), u, f.workspace);
|
||||
REQUIRE_FALSE(rejected.has_value());
|
||||
CHECK(rejected.error().reason == backend::PreparationRejectionReason::NON_FINITE);
|
||||
CHECK(rejected.error().element >= 0);
|
||||
CHECK_FALSE(op.is_prepared());
|
||||
CHECK_THROWS_AS(op.prepare(state(f, rho), u, f.workspace), std::domain_error);
|
||||
rho = 1.0;
|
||||
auto recovered = make_operator(f, 1.0);
|
||||
REQUIRE(recovered.try_prepare(state(f, rho), u, f.workspace).has_value());
|
||||
CHECK(recovered.is_prepared());
|
||||
}
|
||||
|
||||
TEST_CASE("Global scalar bindings retain one coefficient and reject stale meshes", "[equations][mass_normalization][unit]") {
|
||||
GravityFixture f;
|
||||
auto& binding = f.constant_binding;
|
||||
const auto coefficient = constant(1, 0.39);
|
||||
CHECK(binding.size() == 1);
|
||||
CHECK(binding.full_size() == 1);
|
||||
CHECK(binding.gather(binding.scatter(coefficient))[0] == coefficient[0]);
|
||||
CHECK_THROWS_AS(binding.space(), std::logic_error);
|
||||
CHECK_THROWS_AS(binding.validate_values(constant(2, 0.0)), std::invalid_argument);
|
||||
for (int element = 0; element < f.mesh.GetNE(); ++element) {
|
||||
Vector local;
|
||||
binding.element_values(element, coefficient, local);
|
||||
REQUIRE(local.Size() == 1);
|
||||
CHECK(local[0] == (binding.supports(element) ? coefficient[0] : 0.0));
|
||||
}
|
||||
f.mesh.GetElement(0)->SetAttribute(3);
|
||||
CHECK_THROWS(binding.validate());
|
||||
}
|
||||
31
tests/equations/meson.build
Normal file
31
tests/equations/meson.build
Normal file
@@ -0,0 +1,31 @@
|
||||
equation_test_sources = files(
|
||||
'reference_tables_meanfield.cpp',
|
||||
'basis_lifetime_meanfield.cpp',
|
||||
'pressure_boundary.cpp',
|
||||
'fixture.cpp',
|
||||
'manual_closure.cpp',
|
||||
'closure_kernels.cpp',
|
||||
'closure_prepared.cpp',
|
||||
'closure_conditions.cpp',
|
||||
'closure_dilation.cpp',
|
||||
'state_validation.cpp',
|
||||
'equation_contracts.cpp',
|
||||
'gravity_port_fixture.cpp',
|
||||
'h1_divergence.cpp',
|
||||
'h1_gradient.cpp',
|
||||
'h1_gradient_equation.cpp',
|
||||
'h1_vector_gradient.cpp',
|
||||
'h1_vector_gradient_equation.cpp',
|
||||
'barycenter.cpp',
|
||||
'barotropic_density.cpp',
|
||||
'barotropic_density_assembly.cpp',
|
||||
'surface_potential_test.cpp',
|
||||
'mass_normalization.cpp',
|
||||
'angular_momentum.cpp',
|
||||
'pressure_surface.cpp',
|
||||
'tensor_basis.cpp',
|
||||
'serial_backend.cpp',
|
||||
'computation_graphs.cpp',
|
||||
'contribution_lifetimes.cpp',
|
||||
'scoped_computations.cpp',
|
||||
)
|
||||
159
tests/equations/pressure_boundary.cpp
Normal file
159
tests/equations/pressure_boundary.cpp
Normal file
@@ -0,0 +1,159 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <tuple>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include "serif/backend/mfem/discretization/boundary_trace.hpp"
|
||||
#include "serif/backend/mfem/geometry/surface/pressure_boundary.hpp"
|
||||
#include "serif/tests/equations/gravity_fixture.hpp"
|
||||
#include "serif/tests/equations/volume_equation_fixture.hpp"
|
||||
#include "serif/tests/surface/boundary_fixture.hpp"
|
||||
|
||||
namespace {
|
||||
namespace test = serif::tests::equations;
|
||||
namespace gravity = test::gravity;
|
||||
namespace blocks = serif::discretization::blocks;
|
||||
namespace physics = serif::geometry::surface;
|
||||
namespace surface = physics::backend::mfem;
|
||||
namespace volume = serif::operators::backend::mfem;
|
||||
namespace dependencies = serif::utils::dependencies;
|
||||
using Vector = mfem::Vector;
|
||||
using Enthalpy = ::serif::tests::equations::Enthalpy;
|
||||
|
||||
/** @brief Identify boundary coefficients that depend on several native independent coordinates. */
|
||||
[[nodiscard]] std::vector<int> hanging_boundary_dofs(const mfem::FiniteElementSpace& space, const mfem::Array<int>& attributes) {
|
||||
const mfem::SparseMatrix* prolongation = space.GetConformingProlongation();
|
||||
REQUIRE(prolongation != nullptr);
|
||||
mfem::Array<int> boundary;
|
||||
space.GetEssentialVDofs(attributes, boundary);
|
||||
std::vector<int> hanging;
|
||||
for (int local = 0; local < boundary.Size(); ++local) {
|
||||
if (boundary[local] == 0) {
|
||||
continue;
|
||||
}
|
||||
int contributors = 0;
|
||||
for (int entry = prolongation->GetI()[local]; entry < prolongation->GetI()[local + 1]; ++entry) {
|
||||
contributors += prolongation->GetData()[entry] != 0.0;
|
||||
}
|
||||
if (contributors > 1) {
|
||||
hanging.push_back(local);
|
||||
}
|
||||
}
|
||||
return hanging;
|
||||
}
|
||||
|
||||
/** @brief Evaluate an MFEM field at native restriction representatives, without backend trace or reduction helpers. */
|
||||
[[nodiscard]] Vector native_boundary_samples(const mfem::FiniteElementSpace& row_space,
|
||||
const mfem::Array<int>& independent_rows, const mfem::GridFunction& field) {
|
||||
const mfem::SparseMatrix* restriction = row_space.GetConformingRestriction();
|
||||
REQUIRE(restriction != nullptr);
|
||||
std::vector<int> selected(row_space.GetVSize(), -1);
|
||||
for (int row = 0; row < independent_rows.Size(); ++row) {
|
||||
const int independent = independent_rows[row];
|
||||
const int entry = restriction->GetI()[independent];
|
||||
REQUIRE(restriction->GetI()[independent + 1] == entry + 1);
|
||||
REQUIRE(restriction->GetData()[entry] == 1.0);
|
||||
selected[restriction->GetJ()[entry]] = row;
|
||||
}
|
||||
Vector samples(independent_rows.Size());
|
||||
samples = std::numeric_limits<double>::quiet_NaN();
|
||||
mfem::Array<int> dofs;
|
||||
for (int element = 0; element < row_space.GetNE(); ++element) {
|
||||
row_space.GetElementDofs(element, dofs);
|
||||
const mfem::IntegrationRule& nodes = row_space.GetFE(element)->GetNodes();
|
||||
for (int local = 0; local < dofs.Size(); ++local) {
|
||||
const int row = selected[mfem::FiniteElementSpace::DecodeDof(dofs[local])];
|
||||
if (row < 0 || std::isfinite(samples[row])) {
|
||||
continue;
|
||||
}
|
||||
samples[row] = field.GetValue(element, nodes.IntPoint(local));
|
||||
}
|
||||
}
|
||||
for (int row = 0; row < samples.Size(); ++row) {
|
||||
REQUIRE(std::isfinite(samples[row]));
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Nonconforming boundary interpolation retains weighted traces across scalar orders", "[unit][equations][pressure_boundary][nonconforming]") {
|
||||
namespace finite_elements = serif::discretization::backend::mfem;
|
||||
namespace boundary = serif::tests::surface::boundary;
|
||||
using StellarSurface = serif::discretization::domain::StellarSurfaceBoundary;
|
||||
|
||||
mfem::Mesh mesh = boundary::make_boundary_mesh();
|
||||
mesh.EnsureNCMesh();
|
||||
const mfem::Array<int> refinement{0};
|
||||
mesh.GeneralRefinement(refinement);
|
||||
mfem::H1_FECollection cubic_collection(3, 3), quadratic_collection(2, 3);
|
||||
mfem::FiniteElementSpace row_space(&mesh, &cubic_collection), input_space(&mesh, &quadratic_collection);
|
||||
finite_elements::TermBinding<Enthalpy, boundary::Schema> row_binding(row_space);
|
||||
finite_elements::TermBinding<boundary::QuadraticComposition, boundary::Schema> input_binding(input_space);
|
||||
REQUIRE(row_space.GetTrueVSize() < row_space.GetVSize());
|
||||
REQUIRE(input_space.GetTrueVSize() < input_space.GetVSize());
|
||||
// Every cell is material, so supported coordinates have the native true ordering.
|
||||
REQUIRE(row_binding.size() == row_space.GetTrueVSize());
|
||||
REQUIRE(input_binding.size() == input_space.GetTrueVSize());
|
||||
const mfem::Array<int> attributes{1, 0};
|
||||
REQUIRE_FALSE(hanging_boundary_dofs(row_space, attributes).empty());
|
||||
const std::vector<int> input_hanging = hanging_boundary_dofs(input_space, attributes);
|
||||
REQUIRE_FALSE(input_hanging.empty());
|
||||
|
||||
const auto selected = std::make_shared<const finite_elements::DofSelection>(
|
||||
finite_elements::select_boundary_dofs<StellarSurface>(row_binding));
|
||||
mfem::Array<int> native_selected;
|
||||
row_space.GetEssentialTrueDofs(attributes, native_selected);
|
||||
REQUIRE(selected->size() == native_selected.Size());
|
||||
for (int row = 0; row < selected->size(); ++row) {
|
||||
CHECK(selected->reduced_dofs()[row] == native_selected[row]);
|
||||
}
|
||||
const auto trace = finite_elements::make_boundary_trace(selected, input_binding);
|
||||
const Vector values = boundary::sequence(input_binding.size(), 0.8, 0.37);
|
||||
mfem::GridFunction field(&input_space);
|
||||
field.SetFromTrueDofs(values);
|
||||
const Vector expected = native_boundary_samples(row_space, native_selected, field);
|
||||
Vector actual(selected->size());
|
||||
trace.interpolation().Mult(values, actual);
|
||||
CHECK(test::relative_error(actual, expected) < 2.0e-12);
|
||||
|
||||
// This deliberately broken local copy proves that weighted boundary inputs affect the tested rows.
|
||||
mfem::GridFunction missing_hanging_values(&input_space);
|
||||
missing_hanging_values = field;
|
||||
for (const int local : input_hanging) {
|
||||
missing_hanging_values[local] = 0.0;
|
||||
}
|
||||
Vector omitted = native_boundary_samples(row_space, native_selected, missing_hanging_values);
|
||||
omitted -= expected;
|
||||
REQUIRE(omitted.Norml2() > 1.0e-3);
|
||||
|
||||
const Vector dual = boundary::sequence(selected->size(), -0.2, 0.81);
|
||||
Vector transpose(input_binding.size());
|
||||
trace.interpolation().MultTranspose(dual, transpose);
|
||||
CHECK(std::abs(values * transpose - actual * dual) < 2.0e-12 * std::max(1.0, std::abs(actual * dual)));
|
||||
|
||||
Vector zero_trace(row_binding.size());
|
||||
zero_trace = 1.0;
|
||||
for (const int row : selected->reduced_dofs()) {
|
||||
zero_trace[row] = 0.0;
|
||||
}
|
||||
mfem::GridFunction constrained(&row_space);
|
||||
constrained.SetFromTrueDofs(zero_trace);
|
||||
double unselected_boundary_maximum = 0.0;
|
||||
for (int face = 0; face < mesh.GetNBE(); ++face) {
|
||||
mfem::ElementTransformation& transformation = *mesh.GetBdrElementTransformation(face);
|
||||
const mfem::IntegrationRule& rule = mfem::IntRules.Get(transformation.GetGeometryType(), 8);
|
||||
for (int sample = 0; sample < rule.GetNPoints(); ++sample) {
|
||||
const double value = constrained.GetValue(transformation, rule.IntPoint(sample));
|
||||
if (mesh.GetBdrAttribute(face) == 1) {
|
||||
CHECK(std::abs(value) < 2.0e-13);
|
||||
} else {
|
||||
unselected_boundary_maximum = std::max(unselected_boundary_maximum, std::abs(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
CHECK(unselected_boundary_maximum > 0.5);
|
||||
}
|
||||
316
tests/equations/pressure_surface.cpp
Normal file
316
tests/equations/pressure_surface.cpp
Normal file
@@ -0,0 +1,316 @@
|
||||
#include "serif/utils/error/errors.hpp"
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include "serif/equations/barotropic_density.hpp"
|
||||
#include "serif/equations/mass_normalization.hpp"
|
||||
#include "serif/backend/mfem/geometry/surface/pressure_boundary.hpp"
|
||||
#include "serif/geometry/surface/evaluation.hpp"
|
||||
#include "serif/tests/equations/volume_equation_fixture.hpp"
|
||||
#include "serif/tests/equations/gravity_port.hpp"
|
||||
|
||||
namespace {
|
||||
using namespace serif;
|
||||
using namespace serif::tests::equations;
|
||||
using namespace serif::tests::equations::gravity;
|
||||
namespace physics = serif::geometry::surface;
|
||||
namespace surface = physics::backend::mfem;
|
||||
namespace dependencies = serif::utils::dependencies;
|
||||
using Enthalpy = ::serif::tests::equations::Enthalpy;
|
||||
using SurfaceState = dependencies::Bindings<dependencies::Binding<dimensions::MassDensity, Density>>;
|
||||
using Formulation = physics::PressureSurfaceFormulation<eos::models::Polytrope, SurfaceState>;
|
||||
using EnthalpyState = dependencies::Bindings<dependencies::Binding<dimensions::SpecificEnthalpy, Enthalpy>>;
|
||||
using EnthalpyFormulation = physics::PressureSurfaceFormulation<eos::models::Polytrope, EnthalpyState>;
|
||||
using Balance = serif::equations::BarotropicDensityBalance<eos::models::Polytrope>;
|
||||
using Vector = ::mfem::Vector;
|
||||
|
||||
/** @brief Bind the carrier without introducing unrelated field requirements. */
|
||||
auto enthalpy_state(GravityFixture& f, const Vector& h) {
|
||||
return backend::make_term_coefficients(backend::bind(f.enthalpy_binding, h));
|
||||
}
|
||||
|
||||
/** @brief Retain every density-balance column when testing strong boundary replacement. */
|
||||
auto balance_state(GravityFixture& f, const Vector& rho, const Vector& phi, const Vector& C, const Vector& omega) {
|
||||
return backend::make_term_coefficients(backend::bind(f.density_binding, rho), backend::bind(f.potential_binding, phi),
|
||||
backend::bind(f.constant_binding, C), backend::bind(f.angular_velocity_binding, omega));
|
||||
}
|
||||
|
||||
/** @brief Track both polytropic parameters alongside the prescribed pressure and current coefficients. */
|
||||
template <typename Boundary, typename Coefficients>
|
||||
auto pressure_inputs(const std::shared_ptr<const Boundary>& boundary, const eos::models::Polytrope& model, dimensions::PressureValue pressure, const Coefficients& coefficients) {
|
||||
const auto equation_of_state = dependencies::borrow<physics::keys::EquationOfState>(model, dependencies::track_by([](const eos::models::Polytrope& eos) {
|
||||
return std::pair{eos.polytropic_index(), eos.polytropic_constant()};
|
||||
}));
|
||||
return surface::make_pressure_boundary_inputs(boundary, equation_of_state, physics::Isobaric{pressure}, coefficients);
|
||||
}
|
||||
|
||||
/** @brief Constrain the Cartesian fixture's stellar x=0 face, leaving its other faces free. */
|
||||
void tag_cartesian_boundary(GravityFixture& f) {
|
||||
for (int boundary = 0; boundary < f.mesh.GetNBE(); ++boundary) {
|
||||
auto& transform = *f.mesh.GetBdrElementTransformation(boundary);
|
||||
Vector center(3);
|
||||
transform.Transform(::mfem::Geometries.GetCenter(transform.GetGeometryType()), center);
|
||||
f.mesh.GetBdrElement(boundary)->SetAttribute(std::abs(center[0]) < 1.0e-14 ? 1 : 2);
|
||||
}
|
||||
f.mesh.SetAttributes();
|
||||
}
|
||||
|
||||
/** @brief Select essential DOFs independently with MFEM, without the boundary trace compiler. */
|
||||
Vector boundary_mask(GravityFixture& f) {
|
||||
::mfem::Array<int> attributes(f.mesh.bdr_attributes.Max()), marker;
|
||||
attributes = 0;
|
||||
attributes[0] = 1;
|
||||
f.density_space.GetEssentialVDofs(attributes, marker);
|
||||
Vector full(marker.Size());
|
||||
for (int dof = 0; dof < marker.Size(); ++dof) {
|
||||
full[dof] = marker[dof] ? 1.0 : 0.0;
|
||||
}
|
||||
return f.density_binding.gather(full);
|
||||
}
|
||||
|
||||
/** @brief Preserve MeanField prepared_stellar_equilibrium.cpp:810 profiles and surface-row tolerances. */
|
||||
void check_rows(GravityFixture& f) {
|
||||
CAPTURE(f.mesh.GetNE(), f.mesh.GetElementGeometry(0), f.displacement_space.GetOrdering());
|
||||
f.set_compactification();
|
||||
const eos::models::Polytrope eos{3.0, 0.25};
|
||||
const dimensions::PressureValue pressure{0.03125};
|
||||
// For n=3 and K=1/4, rho(P) = (P/K)^(3/4), independently of the EOS implementation.
|
||||
const double required_rho = std::pow(pressure.value() / 0.25, 0.75);
|
||||
const auto rho = f.project(f.density_binding, [](const Vector& x) {
|
||||
return 0.82 + 0.08 * std::sin(0.62 * x[0] + 0.53) + 0.045 * std::cos(0.57 * x[1] - 0.53) + 0.02 * x[2] * x[2];
|
||||
});
|
||||
const auto drho = f.project(f.density_binding, [](const Vector& x) {
|
||||
return 0.21 * std::sin(0.74 * x[0] + 0.97) - 0.14 * std::cos(0.64 * x[1] - 0.97) + 0.075 * x[2];
|
||||
});
|
||||
const auto phi = f.project(f.potential_binding, [](const Vector& x) {
|
||||
return 0.24 + 0.09 * std::sin(0.67 * x[0] + 0.47) - 0.06 * std::cos(0.53 * x[1] - 0.47) + 0.035 * x[2];
|
||||
});
|
||||
const auto dphi = f.project(f.potential_binding, [](const Vector& x) {
|
||||
return 0.17 * std::sin(0.81 * x[0] + 0.89) + 0.11 * std::cos(0.69 * x[1] - 0.89) - 0.07 * x[2];
|
||||
});
|
||||
const auto C = constant(1, 1.07), dC = constant(1, -0.37);
|
||||
const auto omega = constant(1, 0.82), domega = constant(1, 0.13);
|
||||
// Volume displacement is already supported; surface-parameter extension is deliberately not used.
|
||||
const auto u = f.displacement(0.08), du = f.displacement(-0.11);
|
||||
const auto state = balance_state(f, rho, phi, C, omega);
|
||||
const auto direction = balance_state(f, drho, dphi, dC, domega);
|
||||
VolumeEquationFixture balance{Balance{eos, pressure}, f.density_binding, f.displacement_binding,
|
||||
f.compactification, f.mapper, reference_policy()};
|
||||
const auto boundary = surface::make_pressure_boundary<Formulation>(state);
|
||||
const auto inputs = pressure_inputs(boundary, eos, pressure, state);
|
||||
auto cache = dependencies::make_computation_cache<physics::SurfaceFailure>(surface::make_pressure_boundary_graph<Formulation>(), inputs);
|
||||
const auto mask = boundary_mask(f);
|
||||
REQUIRE(boundary->selected_rows->size() > 0);
|
||||
REQUIRE(boundary->selected_rows->size() < f.density_binding.size());
|
||||
CHECK(boundary->selected_rows->size() == static_cast<int>(mask.Sum()));
|
||||
int previous = -1;
|
||||
for (int dof : boundary->selected_rows->reduced_dofs()) {
|
||||
CHECK(dof > previous);
|
||||
CHECK(mask[dof] == 1.0);
|
||||
previous = dof;
|
||||
}
|
||||
|
||||
balance.prepare(state, u, f.workspace);
|
||||
REQUIRE(cache.try_prepare(inputs));
|
||||
Vector residual, action;
|
||||
balance.residual(f.workspace, residual);
|
||||
balance.apply_jacobian(direction, du, f.workspace, action);
|
||||
const auto interior_residual = residual, interior_action = action;
|
||||
surface::apply_residual_rows(*boundary, cache.get<physics::keys::Linearization>(), residual);
|
||||
surface::apply_jacobian_rows(*boundary, cache.get<physics::keys::Linearization>(), direction, action);
|
||||
for (int row = 0; row < rho.Size(); ++row) {
|
||||
CAPTURE(row);
|
||||
if (mask[row]) {
|
||||
CHECK(std::abs(residual[row] - (rho[row] - required_rho)) < 1.0e-13);
|
||||
CHECK(std::abs(action[row] - drho[row]) < 1.0e-13);
|
||||
} else {
|
||||
CHECK(residual[row] == interior_residual[row]);
|
||||
CHECK(action[row] == interior_action[row]);
|
||||
}
|
||||
}
|
||||
|
||||
const auto zero_rho = constant(rho.Size(), 0.0);
|
||||
const auto other_columns = balance_state(f, zero_rho, dphi, dC, domega);
|
||||
Vector other_action;
|
||||
balance.apply_jacobian(other_columns, du, f.workspace, other_action);
|
||||
const auto before_replacement = other_action;
|
||||
surface::apply_jacobian_rows(*boundary, cache.get<physics::keys::Linearization>(), other_columns, other_action);
|
||||
double removed_action = 0.0;
|
||||
for (int row = 0; row < rho.Size(); ++row) {
|
||||
if (mask[row]) {
|
||||
CHECK(other_action[row] == 0.0);
|
||||
removed_action += std::abs(before_replacement[row]);
|
||||
} else {
|
||||
CHECK(other_action[row] == before_replacement[row]);
|
||||
}
|
||||
}
|
||||
CHECK(removed_action > 0.0);
|
||||
|
||||
// The constrained density-balance row retains the old complete-root difference step and threshold.
|
||||
constexpr double step = 1.0e-5;
|
||||
const auto shifted_residual = [&](double scale) {
|
||||
auto trial_rho = rho, trial_phi = phi, trial_C = C, trial_omega = omega, trial_u = u;
|
||||
trial_rho.Add(scale, drho); trial_phi.Add(scale, dphi); trial_C.Add(scale, dC);
|
||||
trial_omega.Add(scale, domega); trial_u.Add(scale, du);
|
||||
const auto trial = balance_state(f, trial_rho, trial_phi, trial_C, trial_omega);
|
||||
balance.prepare(trial, trial_u, f.workspace);
|
||||
REQUIRE(cache.try_prepare(pressure_inputs(boundary, eos, pressure, trial)));
|
||||
Vector result;
|
||||
balance.residual(f.workspace, result);
|
||||
surface::apply_residual_rows(*boundary, cache.get<physics::keys::Linearization>(), result);
|
||||
return result;
|
||||
};
|
||||
auto difference = shifted_residual(step);
|
||||
difference -= shifted_residual(-step);
|
||||
difference /= 2.0 * step;
|
||||
auto error = action;
|
||||
error -= difference;
|
||||
CHECK(error.Norml2() / std::max({action.Norml2(), difference.Norml2(), 100.0 * std::numeric_limits<double>::epsilon()}) < 2.0e-6);
|
||||
}
|
||||
|
||||
/** @brief A second EOS prevents assumptions of a polytrope or h(0)=0. */
|
||||
struct OffsetEOS {
|
||||
using Relations = eos::relations::RelationCatalog<eos::relations::SpecificEnthalpyFromPressure>;
|
||||
double offset{0.3};
|
||||
/** @brief Give zero pressure a nonzero enthalpy that changes with the EOS parameters. */
|
||||
dimensions::SpecificEnthalpyValue evaluate(eos::relations::SpecificEnthalpyFromPressure, dimensions::PressureValue pressure) const {
|
||||
return dimensions::SpecificEnthalpyValue{offset + 2.0 * pressure.value()};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
TEST_CASE("Surface pressure resolves the prescribed quantity through the EOS", "[equations][pressure_surface][unit]") {
|
||||
// MeanField constant_surface_compilation.cpp: pressure-only contract and exact polytropic residual/derivative.
|
||||
const eos::models::Polytrope eos{3.0, 0.25};
|
||||
const dimensions::PressureValue pressure{0.03125};
|
||||
const physics::Isobaric condition{pressure};
|
||||
CHECK(condition.target_pressure().value() == pressure.value());
|
||||
physics::SurfacePoint<Formulation> point{};
|
||||
point.fields.get<Density>() = eos::evaluate<dimensions::MassDensity>(eos, pressure).value();
|
||||
const auto result = physics::evaluate_pressure_surface<Formulation>(eos, condition, point);
|
||||
REQUIRE(result);
|
||||
CHECK(result->residual == 0.0);
|
||||
CHECK(result->field_derivatives.get<Density>() * -0.19 == -0.19);
|
||||
CHECK(result->position_derivative == discretization::forms::VectorValue{});
|
||||
for (double invalid : {-0.1, std::numeric_limits<double>::infinity(), std::numeric_limits<double>::quiet_NaN()}) {
|
||||
CHECK_THROWS_AS((physics::Isobaric{dimensions::PressureValue{invalid}}), std::invalid_argument);
|
||||
}
|
||||
OffsetEOS offset_eos;
|
||||
using OffsetFormulation = physics::PressureSurfaceFormulation<OffsetEOS, EnthalpyState>;
|
||||
physics::SurfacePoint<OffsetFormulation> offset_point{};
|
||||
offset_point.fields.get<Enthalpy>() = 0.3;
|
||||
const physics::Isobaric zero_pressure{dimensions::PressureValue{0.0}};
|
||||
const auto original = physics::evaluate_pressure_surface<OffsetFormulation>(offset_eos, zero_pressure, offset_point);
|
||||
REQUIRE(original);
|
||||
CHECK(original->residual == 0.0);
|
||||
offset_eos.offset = 1.2;
|
||||
const auto updated = physics::evaluate_pressure_surface<OffsetFormulation>(offset_eos, zero_pressure, offset_point);
|
||||
REQUIRE(updated);
|
||||
CHECK(updated->residual == 0.3 - 1.2);
|
||||
CHECK(original->residual == 0.0);
|
||||
offset_eos.offset = std::numeric_limits<double>::infinity();
|
||||
const auto rejected = physics::evaluate_pressure_surface<OffsetFormulation>(offset_eos, condition, offset_point);
|
||||
REQUIRE_FALSE(rejected);
|
||||
CHECK(rejected.error().reason == physics::SurfaceFailureReason::non_finite_eos_response);
|
||||
}
|
||||
|
||||
TEST_CASE("Pressure constraints replace density-balance boundary rows at the original accuracy", "[equations][pressure_surface][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);
|
||||
tag_cartesian_boundary(f);
|
||||
check_rows(f);
|
||||
}
|
||||
}
|
||||
auto fixture = gravity_port::stellar_fixture();
|
||||
check_rows(*fixture);
|
||||
}
|
||||
|
||||
TEST_CASE("Surface pressure preserves exact analytic closure density-balance and mass rows", "[equations][pressure_surface][integration]") {
|
||||
// MeanField prepared_stellar_equilibrium.cpp:979. Retain all physical constants and numerical thresholds.
|
||||
auto fixture = gravity_port::stellar_fixture();
|
||||
auto& f = *fixture;
|
||||
const eos::models::Polytrope eos{1.0, 0.25};
|
||||
const dimensions::SpecificEnthalpyValue enthalpy{0.60};
|
||||
const double density = eos::evaluate<dimensions::MassDensity>(eos, enthalpy).value();
|
||||
const auto pressure = eos::evaluate<dimensions::Pressure>(eos, enthalpy);
|
||||
double volume = 0.0;
|
||||
for (int element = 0; element < f.mesh.GetNE(); ++element) {
|
||||
if (!f.enthalpy_binding.supports(element)) {
|
||||
continue;
|
||||
}
|
||||
auto& transform = *f.mesh.GetElementTransformation(element);
|
||||
const auto& rule = ::mfem::IntRules.Get(f.mesh.GetElementGeometry(element), reference_order);
|
||||
for (int q = 0; q < rule.GetNPoints(); ++q) {
|
||||
const auto& ip = rule.IntPoint(q);
|
||||
transform.SetIntPoint(&ip);
|
||||
volume += ip.weight * transform.Weight();
|
||||
}
|
||||
}
|
||||
const double target_mass = density * volume;
|
||||
const auto rho = constant(f.density_binding.size(), density), h = constant(f.enthalpy_binding.size(), enthalpy.value());
|
||||
// A uniform state at the surface density has A(rho) = A(rho_surface), so the balance closes at Us = phi.
|
||||
const auto phi = constant(f.potential_binding.size(), 0.20), C = constant(1, 0.20);
|
||||
const auto omega = constant(1, 0.0), u = constant(f.displacement_binding.size(), 0.0);
|
||||
const auto state = backend::make_term_coefficients(backend::bind(f.density_binding, rho), backend::bind(f.enthalpy_binding, h),
|
||||
backend::bind(f.potential_binding, phi), backend::bind(f.constant_binding, C),
|
||||
backend::bind(f.angular_velocity_binding, omega));
|
||||
VolumeEquationFixture closure{Closure{eos}, f.density_binding, f.displacement_binding, f.compactification, f.mapper, reference_policy()};
|
||||
VolumeEquationFixture balance{Balance{eos, pressure}, f.density_binding, f.displacement_binding, f.compactification, f.mapper, reference_policy()};
|
||||
VolumeEquationFixture mass{serif::equations::MassNormalization{target_mass}, f.constant_binding,
|
||||
f.displacement_binding, f.compactification, f.mapper, reference_policy()};
|
||||
const auto boundary = surface::make_pressure_boundary<Formulation>(state);
|
||||
const auto inputs = pressure_inputs(boundary, eos, pressure, state);
|
||||
auto cache = dependencies::make_computation_cache<physics::SurfaceFailure>(surface::make_pressure_boundary_graph<Formulation>(), inputs);
|
||||
closure.prepare(state, u, f.workspace);
|
||||
balance.prepare(state, u, f.workspace);
|
||||
mass.prepare(state, u, f.workspace);
|
||||
REQUIRE(cache.try_prepare(inputs));
|
||||
Vector result;
|
||||
closure.residual(f.workspace, result);
|
||||
CHECK(result.Norml2() < 2.0e-12);
|
||||
balance.residual(f.workspace, result);
|
||||
surface::apply_residual_rows(*boundary, cache.get<physics::keys::Linearization>(), result);
|
||||
CHECK(result.Norml2() < 2.0e-12);
|
||||
mass.residual(f.workspace, result);
|
||||
REQUIRE(result.Size() == 1);
|
||||
CHECK(std::abs(result[0]) < 2.0e-11 * target_mass);
|
||||
}
|
||||
|
||||
TEST_CASE("Boundary selection requires supported tagged nodal coefficients", "[equations][pressure_surface][unit]") {
|
||||
GravityFixture f;
|
||||
tag_cartesian_boundary(f);
|
||||
const Vector h = constant(f.enthalpy_binding.size(), 0.7);
|
||||
const auto state = enthalpy_state(f, h);
|
||||
SECTION("Absent boundary") {
|
||||
for (int boundary = 0; boundary < f.mesh.GetNBE(); ++boundary) {
|
||||
f.mesh.GetBdrElement(boundary)->SetAttribute(2);
|
||||
}
|
||||
CHECK_THROWS_AS(surface::make_pressure_boundary<EnthalpyFormulation>(state), std::invalid_argument);
|
||||
}
|
||||
SECTION("Boundary outside the stellar support") {
|
||||
for (int boundary = 0; boundary < f.mesh.GetNBE(); ++boundary) {
|
||||
f.mesh.GetBdrElement(boundary)->SetAttribute(1);
|
||||
}
|
||||
CHECK_THROWS_AS(surface::make_pressure_boundary<EnthalpyFormulation>(state), std::invalid_argument);
|
||||
}
|
||||
SECTION("Positive H1 coefficients are not nodal values") {
|
||||
::mfem::H1_FECollection positive{Enthalpy::FunctionalSpace::order, 3, ::mfem::BasisType::Positive};
|
||||
::mfem::FiniteElementSpace space{&f.mesh, &positive};
|
||||
discretization::backend::mfem::TermBinding<Enthalpy, Schema> binding{space};
|
||||
const Vector values = constant(binding.size(), 0.7);
|
||||
const auto positive_state = backend::make_term_coefficients(backend::bind(binding, values));
|
||||
CHECK_THROWS_AS(surface::make_pressure_boundary<EnthalpyFormulation>(positive_state), std::invalid_argument);
|
||||
}
|
||||
SECTION("Indices must be sorted unique and in range") {
|
||||
using discretization::backend::mfem::DofSelection;
|
||||
for (const ::mfem::Array<int>& indices : {::mfem::Array<int>{-1}, ::mfem::Array<int>{0, 0},
|
||||
::mfem::Array<int>{1, 0}, ::mfem::Array<int>{f.enthalpy_binding.size()}}) {
|
||||
CHECK_THROWS_AS((DofSelection{f.enthalpy_binding, indices}), std::invalid_argument);
|
||||
}
|
||||
CHECK_THROWS_AS(f.enthalpy_binding.reduced_dof(-1), std::out_of_range);
|
||||
CHECK_THROWS_AS(f.enthalpy_binding.reduced_dof(f.enthalpy_binding.full_size()), std::out_of_range);
|
||||
}
|
||||
}
|
||||
92
tests/equations/reference_tables_meanfield.cpp
Normal file
92
tests/equations/reference_tables_meanfield.cpp
Normal file
@@ -0,0 +1,92 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "mfem/fem/fe_coll.hpp"
|
||||
#include "serif/backend/mfem/discretization/basis_evaluation.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace serif::discretization::backend::mfem;
|
||||
|
||||
/// Check every basis coefficient against MFEM, including bases that require dense fallback.
|
||||
void check_native_basis(const ::mfem::FiniteElement& element, const ::mfem::IntegrationRule& rule, TensorBasisCache& cache)
|
||||
{
|
||||
const bool vector = element.GetRangeType() == ::mfem::FiniteElement::VECTOR;
|
||||
const ReferenceOperation operation = vector ? ReferenceOperation::value : ReferenceOperation::value_and_gradient;
|
||||
const int components = vector ? 3 : 4;
|
||||
BasisEvaluation evaluation;
|
||||
evaluation.sample(element, rule, operation, 1, cache, ContractionMode::automatic);
|
||||
BasisWorkspace workspace;
|
||||
::mfem::Vector coefficients(element.GetDof()), values, shape(element.GetDof());
|
||||
::mfem::DenseMatrix derivatives(element.GetDof(), 3);
|
||||
|
||||
for (int dof = 0; dof < element.GetDof(); ++dof)
|
||||
{
|
||||
coefficients = 0.0;
|
||||
coefficients[dof] = 1.0;
|
||||
evaluation.evaluate(coefficients, values, workspace);
|
||||
REQUIRE(std::all_of(values.GetData(), values.GetData() + values.Size(), [](const double value)
|
||||
{
|
||||
return std::isfinite(value);
|
||||
}));
|
||||
double error = 0.0;
|
||||
for (int point = 0; point < rule.GetNPoints(); ++point)
|
||||
{
|
||||
if (vector)
|
||||
{
|
||||
element.CalcVShape(rule.IntPoint(point), derivatives);
|
||||
}
|
||||
else
|
||||
{
|
||||
element.CalcShape(rule.IntPoint(point), shape);
|
||||
element.CalcDShape(rule.IntPoint(point), derivatives);
|
||||
error = std::max(error, std::abs(values[components * point] - shape[dof]));
|
||||
}
|
||||
|
||||
for (int axis = 0; axis < 3; ++axis)
|
||||
{
|
||||
const int sample = components * point + axis + (vector ? 0 : 1);
|
||||
error = std::max(error, std::abs(values[sample] - derivatives(dof, axis)));
|
||||
}
|
||||
}
|
||||
CHECK(error < 2.0e-12);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("MeanField native scalar reference values and gradients include positive and closed bases", "[equations][tensor_basis][unit]")
|
||||
{
|
||||
for (const int order : {1, 3})
|
||||
{
|
||||
::mfem::H1_FECollection h1(order, 3);
|
||||
::mfem::L2_FECollection l2(order - 1, 3);
|
||||
TensorBasisCache cache;
|
||||
const ::mfem::IntegrationRule& rule = ::mfem::IntRules.Get(::mfem::Geometry::CUBE, 2 * order + 1);
|
||||
check_native_basis(*h1.FiniteElementForGeometry(::mfem::Geometry::CUBE), rule, cache);
|
||||
check_native_basis(*l2.FiniteElementForGeometry(::mfem::Geometry::CUBE), rule, cache);
|
||||
}
|
||||
|
||||
::mfem::H1_FECollection nodal(3, 3), positive(3, 3, ::mfem::BasisType::Positive);
|
||||
::mfem::L2_FECollection open(3, 3), closed(3, 3, ::mfem::BasisType::GaussLobatto);
|
||||
TensorBasisCache cache;
|
||||
const ::mfem::IntegrationRule& rule = ::mfem::IntRules.Get(::mfem::Geometry::CUBE, 5);
|
||||
std::array<std::shared_ptr<const TensorBasis>, 4> plans;
|
||||
const std::array<const ::mfem::FiniteElementCollection*, 4> collections{&nodal, &positive, &open, &closed};
|
||||
for (std::size_t index = 0; index < collections.size(); ++index)
|
||||
{
|
||||
const ::mfem::FiniteElement& element = *collections[index]->FiniteElementForGeometry(::mfem::Geometry::CUBE);
|
||||
check_native_basis(element, rule, cache);
|
||||
plans[index] = cache.get(element, rule, ReferenceOperation::value_and_gradient, 1);
|
||||
if (plans[index])
|
||||
{
|
||||
for (std::size_t previous = 0; previous < index; ++previous)
|
||||
{
|
||||
CHECK(plans[index] != plans[previous]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
198
tests/equations/scoped_computations.cpp
Normal file
198
tests/equations/scoped_computations.cpp
Normal file
@@ -0,0 +1,198 @@
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <memory>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#include <variant>
|
||||
#include "serif/backend/mfem/model/volume_displacement.hpp"
|
||||
|
||||
#include "serif/tests/equations/test_equations.hpp"
|
||||
#include "serif/equations/mass_normalization.hpp"
|
||||
#include "serif/tests/equations/fixture.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace serif;
|
||||
using namespace serif::tests::equations;
|
||||
namespace equations_backend = serif::operators::backend::mfem;
|
||||
namespace dependencies = utils::dependencies;
|
||||
using Status = dependencies::PreparationStatus;
|
||||
using Mass = serif::equations::MassNormalization;
|
||||
|
||||
struct FirstClosure
|
||||
{
|
||||
};
|
||||
struct SecondClosure
|
||||
{
|
||||
};
|
||||
struct MassRow
|
||||
{
|
||||
};
|
||||
using First = equations_backend::volume_keys::Scoped<FirstClosure>;
|
||||
using Second = equations_backend::volume_keys::Scoped<SecondClosure>;
|
||||
using MassStages = equations_backend::volume_keys::Scoped<MassRow>;
|
||||
|
||||
/** @brief Reuse the ordinary fixture bindings without introducing an equation-specific owner. */
|
||||
auto closure_discretization(const Fixture& fixture)
|
||||
{
|
||||
return equations_backend::VolumeDiscretization{
|
||||
.residual_binding = fixture.density_binding,
|
||||
.displacement_binding = fixture.displacement_binding,
|
||||
.compactification = fixture.compactification,
|
||||
.mapper = fixture.mapper};
|
||||
}
|
||||
|
||||
/** @brief Compose declarations once and retain the ordinary dependency-cache failure type. */
|
||||
template <typename Context, typename Nodes>
|
||||
auto make_cache(const Context& context, Nodes nodes)
|
||||
{
|
||||
return std::apply([&](auto... node)
|
||||
{
|
||||
return dependencies::make_computation_cache<equations_backend::PreparationRejection>(context, std::move(node)...);
|
||||
},
|
||||
std::move(nodes));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Scoped instances of one volume equation share fields without sharing private stages", "[unit][equations][scoped_computations]")
|
||||
{
|
||||
Fixture fixture;
|
||||
const auto discretization = closure_discretization(fixture);
|
||||
auto density = fixture.density();
|
||||
const auto enthalpy = fixture.enthalpy();
|
||||
const auto displacement = fixture.displacement(0.03);
|
||||
const eos::models::Polytrope first_eos{1.5, 0.25};
|
||||
const eos::models::Polytrope second_eos{3.0, 1.5};
|
||||
const auto inputs = [&]()
|
||||
{
|
||||
const auto coefficients = fixture.state(density, enthalpy);
|
||||
const auto first = equations_backend::make_volume_inputs<Closure, FirstClosure>(discretization, coefficients, displacement);
|
||||
const auto second = equations_backend::make_volume_inputs<Closure, SecondClosure>(discretization, coefficients, displacement);
|
||||
return dependencies::make_dependency_context(
|
||||
first.resource<First::Bindings>(), second.resource<Second::Bindings>(),
|
||||
first.resource<equations_backend::DisplacementCoefficients>(), first.resource<equations_backend::CompactificationCoefficients>(),
|
||||
first.resource<blocks::density::mass>(), first.resource<::serif::tests::equations::Enthalpy>());
|
||||
};
|
||||
auto cache = make_cache(inputs(), std::tuple_cat(
|
||||
equations_backend::make_volume_equation_computations<FirstClosure>(Closure{first_eos}, discretization),
|
||||
equations_backend::make_volume_equation_computations<SecondClosure>(Closure{second_eos}, discretization)));
|
||||
STATIC_CHECK_FALSE(std::same_as<First::Linearization, Second::Linearization>);
|
||||
REQUIRE(cache.try_prepare(inputs()));
|
||||
CHECK(relative_error(cache.get<First::Linearization>().residual, manual_closure(fixture, first_eos, density, enthalpy, displacement)) < 2.0e-12);
|
||||
CHECK(relative_error(cache.get<Second::Linearization>().residual, manual_closure(fixture, second_eos, density, enthalpy, displacement)) < 2.0e-12);
|
||||
const auto density_direction = deterministic(density.Size(), 0.3);
|
||||
const auto enthalpy_direction = deterministic(enthalpy.Size(), 0.7);
|
||||
const auto displacement_direction = fixture.displacement(0.1);
|
||||
const auto direction = packed(density_direction, enthalpy_direction, displacement_direction);
|
||||
const auto dual = deterministic(density.Size(), 0.9);
|
||||
mfem::Vector action, transpose;
|
||||
equations_backend::Workspace workspace;
|
||||
equations_backend::apply_jacobian(cache.get<Second::Linearization>(), direction, workspace, action);
|
||||
CHECK(relative_error(action, manual_closure(fixture, second_eos, density, enthalpy, displacement, &density_direction, &enthalpy_direction, &displacement_direction)) < 2.0e-12);
|
||||
equations_backend::apply_jacobian_transpose(cache.get<Second::Linearization>(), dual, workspace, transpose);
|
||||
CHECK((action * dual) == Catch::Approx(direction * transpose).epsilon(2.0e-12));
|
||||
const auto frozen = cache.share<Second::Linearization>();
|
||||
REQUIRE(cache.try_prepare(inputs()));
|
||||
CHECK(cache.report().computed_count() == 0);
|
||||
CHECK(cache.share<Second::Linearization>() == frozen);
|
||||
|
||||
density += 0.04;
|
||||
REQUIRE(cache.try_prepare(inputs(), dependencies::RequestedResults<First::Linearization>{}));
|
||||
CHECK(cache.report().status<First::Basis<0>>() == Status::reused);
|
||||
CHECK(cache.report().status<First::QuadratureLinearization<0>>() == Status::computed);
|
||||
CHECK_FALSE(cache.contains<Second::Linearization>());
|
||||
CHECK(equations_backend::volume_work_report<FirstClosure>(cache).physics_points > 0);
|
||||
REQUIRE(cache.try_prepare(inputs()));
|
||||
CHECK(cache.report().status<First::Linearization>() == Status::reused);
|
||||
CHECK(cache.report().status<Second::QuadratureLinearization<0>>() == Status::computed);
|
||||
CHECK(cache.share<Second::Linearization>() != frozen);
|
||||
CHECK(relative_error(cache.get<Second::Linearization>().residual, manual_closure(fixture, second_eos, density, enthalpy, displacement)) < 2.0e-12);
|
||||
}
|
||||
|
||||
TEST_CASE("Distinct volume equations prepare in one graph from common coefficient sources", "[unit][equations][scoped_computations]")
|
||||
{
|
||||
Fixture fixture;
|
||||
const eos::models::Polytrope eos{1.5, 0.25};
|
||||
auto density = fixture.density();
|
||||
auto enthalpy = fixture.enthalpy();
|
||||
auto displacement = fixture.displacement(0.03);
|
||||
const auto closure = closure_discretization(fixture);
|
||||
const equations_backend::VolumeDiscretization mass{
|
||||
.residual_binding = fixture.constant_binding,
|
||||
.displacement_binding = fixture.displacement_binding,
|
||||
.compactification = fixture.compactification,
|
||||
.mapper = fixture.mapper};
|
||||
const auto coefficients = [&]()
|
||||
{
|
||||
return equations_backend::make_term_coefficients(equations_backend::bind(fixture.density_binding, density), equations_backend::bind(fixture.enthalpy_binding, enthalpy));
|
||||
};
|
||||
const auto inputs = [&]()
|
||||
{
|
||||
const auto state = coefficients();
|
||||
const auto volume = equations_backend::make_volume_inputs<Closure, FirstClosure>(closure, state, displacement);
|
||||
const auto integral = equations_backend::make_volume_inputs<Mass, MassRow>(mass, state, displacement);
|
||||
return dependencies::make_dependency_context(
|
||||
volume.resource<First::Bindings>(), integral.resource<MassStages::Bindings>(),
|
||||
volume.resource<equations_backend::DisplacementCoefficients>(), volume.resource<equations_backend::CompactificationCoefficients>(),
|
||||
volume.resource<blocks::density::mass>(), volume.resource<::serif::tests::equations::Enthalpy>());
|
||||
};
|
||||
auto cache = make_cache(inputs(), std::tuple_cat(
|
||||
equations_backend::make_volume_equation_computations<FirstClosure>(Closure{eos}, closure),
|
||||
equations_backend::make_volume_equation_computations<MassRow>(Mass{0.8}, mass)));
|
||||
REQUIRE(cache.try_prepare(inputs()));
|
||||
const auto frozen_volume = cache.share<First::Linearization>();
|
||||
REQUIRE(cache.try_prepare(inputs()));
|
||||
CHECK(cache.report().computed_count() == 0);
|
||||
|
||||
enthalpy += 0.03;
|
||||
REQUIRE(cache.try_prepare(inputs()));
|
||||
CHECK(cache.report().status<First::Linearization>() == Status::computed);
|
||||
CHECK(cache.report().status<MassStages::Linearization>() == Status::reused);
|
||||
|
||||
density += 0.02;
|
||||
REQUIRE(cache.try_prepare(inputs()));
|
||||
CHECK(cache.report().status<MassStages::Linearization>() == Status::computed);
|
||||
enthalpy = -1.0;
|
||||
const auto rejected = cache.try_prepare(inputs());
|
||||
REQUIRE_FALSE(rejected);
|
||||
CHECK_FALSE(cache.ready());
|
||||
CHECK_FALSE(cache.contains<MassStages::Linearization>());
|
||||
CHECK(frozen_volume->residual.Size() == fixture.density_binding.size());
|
||||
}
|
||||
|
||||
TEST_CASE("Scoped equation metadata connects the shared deformation displacement source", "[unit][equations][scoped_computations]")
|
||||
{
|
||||
namespace deformation = serif::geometry::deformation;
|
||||
namespace deformation_backend = deformation::backend::mfem;
|
||||
using Failure = std::variant<equations_backend::PreparationRejection, deformation::DeformationFailure>;
|
||||
|
||||
Fixture fixture;
|
||||
const auto discretization = closure_discretization(fixture);
|
||||
const auto density = fixture.density();
|
||||
const auto enthalpy = fixture.enthalpy();
|
||||
const auto displacement = fixture.displacement(0.03);
|
||||
std::vector<double> generated(displacement.GetData(), displacement.GetData() + displacement.Size());
|
||||
const auto reference = deformation_backend::deformation_discretization(fixture.enthalpy_space, fixture.displacement_space, fixture.mesh);
|
||||
const auto volume = equations_backend::make_volume_inputs<Closure, FirstClosure>(discretization, fixture.state(density, enthalpy));
|
||||
const auto context = dependencies::make_dependency_context(
|
||||
volume.resource<First::Bindings>(), volume.resource<equations_backend::CompactificationCoefficients>(),
|
||||
volume.resource<blocks::density::mass>(), volume.resource<::serif::tests::equations::Enthalpy>(),
|
||||
dependencies::own<deformation::keys::ReferenceDiscretization>(reference, deformation_backend::ReferenceDiscretizationTracking{}),
|
||||
dependencies::borrow<deformation::keys::VolumeDisplacement>(generated));
|
||||
const eos::models::Polytrope eos{1.5, 0.25};
|
||||
auto cache = std::apply([&](auto... node)
|
||||
{
|
||||
return dependencies::make_computation_cache<Failure>(context, std::move(node)...);
|
||||
}, std::tuple_cat(
|
||||
deformation_backend::make_volume_displacement_computations<First::Bindings>(fixture.displacement_binding),
|
||||
equations_backend::make_volume_equation_computations<FirstClosure>(Closure{eos}, discretization)));
|
||||
REQUIRE(cache.try_prepare(context));
|
||||
CHECK(relative_error(cache.get<equations_backend::DisplacementCoefficients>().values, displacement) == 0.0);
|
||||
CHECK(relative_error(cache.get<First::Linearization>().residual, manual_closure(fixture, eos, density, enthalpy, displacement)) < 2.0e-12);
|
||||
REQUIRE(cache.try_prepare(context));
|
||||
CHECK(cache.report().computed_count() == 0);
|
||||
generated[0] += 0.001;
|
||||
REQUIRE(cache.try_prepare(context));
|
||||
CHECK(cache.report().status<deformation_backend::volume_displacement_keys::CheckedBinding>() == Status::reused);
|
||||
CHECK(cache.report().status<First::Geometry<0>>() == Status::computed);
|
||||
}
|
||||
440
tests/equations/serial_backend.cpp
Normal file
440
tests/equations/serial_backend.cpp
Normal file
@@ -0,0 +1,440 @@
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
|
||||
#include "serif/tests/equations/gravity_port.hpp"
|
||||
#include "serif/equations/mass_normalization.hpp"
|
||||
#include "serif/equations/angular_momentum.hpp"
|
||||
#include "serif/equations/primal_gravity.hpp"
|
||||
#include "serif/equations/barotropic_density.hpp"
|
||||
#include "serif/backend/mfem/operators/volume_field_block.hpp"
|
||||
#include "serif/tests/nonconforming.hpp"
|
||||
|
||||
namespace {
|
||||
using namespace serif;
|
||||
using namespace serif::tests::equations;
|
||||
using namespace serif::tests::equations::gravity;
|
||||
using Vector = ::mfem::Vector;
|
||||
using Mode = backend::PreparationMode;
|
||||
|
||||
struct Values {
|
||||
Vector rho, h, phi, c, omega, u;
|
||||
/// Smooth values covering every migrated equation's unknowns.
|
||||
explicit Values(GravityFixture& f) : rho(f.density()), h(f.enthalpy()), phi(f.potential()),
|
||||
c(constant(1, 0.7)), omega(constant(1, 0.4)), u(f.displacement(0.04)) {}
|
||||
/// Bind each physical term once, independently of which equation consumes it.
|
||||
auto state(GravityFixture& f) const {
|
||||
return backend::make_term_coefficients(backend::bind(f.density_binding, rho), backend::bind(f.enthalpy_binding, h),
|
||||
backend::bind(f.potential_binding, phi),
|
||||
backend::bind(f.constant_binding, c), backend::bind(f.angular_velocity_binding, omega));
|
||||
}
|
||||
};
|
||||
|
||||
/// Use the prepared plan's public column order to construct an independent packed direction.
|
||||
template <typename OperatorT, typename StateT>
|
||||
Vector pack(const OperatorT& op, const StateT& state, const Vector& displacement) {
|
||||
Vector result(op.width());
|
||||
int offset = 0;
|
||||
OperatorT::Plan::for_each_column([&]<typename OperandT>(OperandT, std::size_t) {
|
||||
const auto& values = state.template get<typename OperandT::term_type>().values;
|
||||
std::copy_n(values.GetData(), values.Size(), result.GetData() + offset);
|
||||
offset += values.Size();
|
||||
});
|
||||
std::copy_n(displacement.GetData(), displacement.Size(), result.GetData() + offset);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Check full field/geometry transposes, dense/tensor agreement, freezing and the MFEM adapter.
|
||||
template <typename EquationT, typename BindingT>
|
||||
void check_operator(GravityFixture& f, EquationT equation, const BindingT& row) {
|
||||
CAPTURE(EquationT::name, f.mesh.GetElementGeometry(0), f.displacement_space.GetOrdering());
|
||||
Values values(f), direction(f);
|
||||
direction.rho = deterministic(values.rho.Size(), 0.3);
|
||||
direction.h = deterministic(values.h.Size(), 0.5);
|
||||
direction.phi = deterministic(values.phi.Size(), 0.9);
|
||||
direction.c[0] = -0.23;
|
||||
direction.omega[0] = 0.17;
|
||||
direction.u = f.displacement(-0.07);
|
||||
VolumeEquationFixture automatic{equation, row, f.displacement_binding, f.compactification, f.mapper, reference_policy()};
|
||||
VolumeEquationFixture dense{equation, row, f.displacement_binding, f.compactification, f.mapper, reference_policy(), backend::ContractionMode::dense};
|
||||
automatic.prepare(values.state(f), values.u, f.workspace);
|
||||
dense.prepare(values.state(f), values.u, f.workspace);
|
||||
CHECK(dense.tensor_element_count() == 0);
|
||||
CHECK((automatic.tensor_element_count() > 0) == (f.mesh.GetElementGeometry(0) == ::mfem::Geometry::CUBE));
|
||||
Vector residual, expected_residual, action, expected_action, transpose, expected_transpose;
|
||||
automatic.residual(f.workspace, residual);
|
||||
dense.residual(f.workspace, expected_residual);
|
||||
CHECK(relative_error(residual, expected_residual) < 2.0e-12);
|
||||
const auto input = pack(automatic, direction.state(f), direction.u);
|
||||
automatic.apply_jacobian(input, f.workspace, action);
|
||||
dense.apply_jacobian(input, f.workspace, expected_action);
|
||||
CHECK(relative_error(action, expected_action) < 2.0e-12);
|
||||
auto dual = deterministic(automatic.height(), -0.8);
|
||||
automatic.apply_jacobian_transpose(dual, f.workspace, transpose);
|
||||
dense.apply_jacobian_transpose(dual, f.workspace, expected_transpose);
|
||||
CHECK(relative_error(transpose, expected_transpose) < 2.0e-12);
|
||||
// The old mass-adapter transpose pairing threshold is retained for every equation.
|
||||
CHECK(gravity_port::pairing_relative(input * transpose, action * dual) < 2.0e-12);
|
||||
backend::JacobianOperator adapter{automatic.cache()};
|
||||
Vector adapted;
|
||||
adapter.MultTranspose(dual, adapted);
|
||||
CHECK(relative_error(adapted, transpose) < 2.0e-15);
|
||||
Vector zero = constant(dual.Size(), 0.0);
|
||||
automatic.apply_jacobian_transpose(zero, f.workspace, adapted);
|
||||
CHECK(adapted.Norml2() == 0.0);
|
||||
|
||||
// Probe both ends of each coefficient block and the geometry block independently.
|
||||
int offset = 0;
|
||||
const auto check_column = [&](int index) {
|
||||
Vector basis = constant(automatic.width(), 0.0), column;
|
||||
basis[index] = 1.0;
|
||||
automatic.apply_jacobian(basis, f.workspace, column);
|
||||
CHECK(std::abs(column * dual - transpose[index]) < 2.0e-12 * std::max(1.0, std::abs(transpose[index])));
|
||||
};
|
||||
decltype(automatic)::Plan::for_each_column([&]<typename OperandT>(OperandT, std::size_t) {
|
||||
const auto& input_values = values.state(f).template get<typename OperandT::term_type>().values;
|
||||
check_column(offset);
|
||||
check_column(offset + input_values.Size() - 1);
|
||||
offset += input_values.Size();
|
||||
});
|
||||
check_column(offset);
|
||||
check_column(automatic.width() - 1);
|
||||
values.rho = values.h = values.phi = 0.0;
|
||||
values.c = values.omega = values.u = 0.0;
|
||||
automatic.apply_jacobian_transpose(dual, f.workspace, adapted);
|
||||
CHECK(relative_error(adapted, transpose) < 2.0e-15);
|
||||
Vector wrong_size(automatic.height() + 1);
|
||||
CHECK_THROWS_AS(automatic.apply_jacobian_transpose(wrong_size, f.workspace, adapted), std::invalid_argument);
|
||||
dual[0] = std::numeric_limits<double>::quiet_NaN();
|
||||
CHECK_THROWS_AS(automatic.apply_jacobian_transpose(dual, f.workspace, adapted), std::domain_error);
|
||||
}
|
||||
|
||||
/** @brief Ensure a material-only equation actually encounters weighted input or test coefficients. */
|
||||
void require_material_hanging_coefficient(const discretization::backend::mfem::DofMap& binding,
|
||||
const discretization::backend::mfem::DofMap& material) {
|
||||
const ::mfem::SparseMatrix* prolongation = binding.space().GetConformingProlongation();
|
||||
REQUIRE(prolongation != nullptr);
|
||||
bool found = false;
|
||||
::mfem::Array<int> dofs;
|
||||
for (int element = 0; element < binding.mesh().GetNE(); ++element) {
|
||||
if (!material.supports(element)) {
|
||||
continue;
|
||||
}
|
||||
binding.space().GetElementVDofs(element, dofs);
|
||||
for (const int encoded : dofs) {
|
||||
const int local = ::mfem::FiniteElementSpace::DecodeDof(encoded);
|
||||
int contributions = 0;
|
||||
for (int entry = prolongation->GetI()[local]; entry < prolongation->GetI()[local + 1]; ++entry) {
|
||||
if (prolongation->GetData()[entry] != 0.0) {
|
||||
++contributions;
|
||||
}
|
||||
}
|
||||
found = found || contributions > 1;
|
||||
}
|
||||
}
|
||||
REQUIRE(found);
|
||||
}
|
||||
|
||||
/** @brief Isolate the production stellar source without assembling vacuum diffusion. */
|
||||
struct StellarDensitySource : serif::equations::PrimalPoissonBalance::DensitySource {
|
||||
static constexpr std::string_view name = "stellar density source";
|
||||
/** @brief Preserve the primal Poisson source coefficient and sign. */
|
||||
explicit StellarDensitySource(const double coefficient) : DensitySource(coefficient) {}
|
||||
};
|
||||
|
||||
struct CountingClosure {
|
||||
using Test = Closure::Test;
|
||||
using Operands = Closure::Operands;
|
||||
static constexpr std::string_view name = "counting closure";
|
||||
Closure equation{eos::models::Polytrope{1.5, 0.25}};
|
||||
std::shared_ptr<int> calls = std::make_shared<int>(0);
|
||||
/// Count local evaluations without changing the physical residual or derivative.
|
||||
auto evaluate(const serif::equations::EquationPoint<Operands>& point) const { ++*calls; return equation.evaluate(point); }
|
||||
/// Use the real closure's quadrature contract.
|
||||
template <typename OperandT> int additional_order(OperandT operand, int order) const { return equation.additional_order(operand, order); }
|
||||
};
|
||||
}
|
||||
|
||||
TEST_CASE("Every migrated volume equation has a complete serial Jacobian transpose", "[equations][serial_backend][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();
|
||||
const eos::models::Polytrope eos{1.5, 0.25};
|
||||
check_operator(f, Closure{eos}, f.density_binding);
|
||||
check_operator(f, PotentialRow{four_pi_G}, f.potential_binding);
|
||||
check_operator(f, serif::equations::BarotropicDensityBalance{eos}, f.density_binding);
|
||||
check_operator(f, serif::equations::MassNormalization{0.8}, f.constant_binding);
|
||||
check_operator(f, serif::equations::AngularMomentumConstraint{0.3}, f.angular_velocity_binding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Curved compactified geometry preserves transpose pairings", "[equations][serial_backend][integration]") {
|
||||
auto fixture = gravity_port::stellar_fixture();
|
||||
auto& f = *fixture;
|
||||
check_operator(f, PotentialRow{four_pi_G}, f.potential_binding);
|
||||
const eos::models::Polytrope eos{1.5, 0.25};
|
||||
check_operator(f, serif::equations::BarotropicDensityBalance{eos}, f.density_binding);
|
||||
}
|
||||
|
||||
TEST_CASE("Selective preparation refreshes only changed operand samples and geometry", "[equations][serial_backend][unit]") {
|
||||
GravityFixture f;
|
||||
f.set_compactification();
|
||||
CountingClosure equation;
|
||||
VolumeEquationFixture op{equation, f.density_binding, f.displacement_binding, f.compactification, f.mapper, reference_policy()};
|
||||
Values values(f);
|
||||
op.prepare(values.state(f), values.u, f.workspace);
|
||||
const auto initial = op.preparation_report();
|
||||
REQUIRE(initial.physics_points > 0);
|
||||
CHECK(initial.physics_points == static_cast<std::size_t>(*equation.calls));
|
||||
const auto initial_calls = *equation.calls;
|
||||
Vector before, after;
|
||||
op.residual(f.workspace, before);
|
||||
op.prepare(values.state(f), values.u, f.workspace, Mode::reuse_unchanged);
|
||||
CHECK_FALSE(op.preparation_report().did_work());
|
||||
CHECK(op.preparation_report().reused_elements > 0);
|
||||
CHECK(*equation.calls == initial_calls);
|
||||
op.residual(f.workspace, after);
|
||||
CHECK(relative_error(after, before) == 0.0);
|
||||
|
||||
values.rho += 0.03;
|
||||
op.prepare(values.state(f), values.u, f.workspace, Mode::reuse_unchanged);
|
||||
CHECK(op.preparation_report().geometry_points == 0);
|
||||
CHECK(op.preparation_report().sampled_operands * 2 == initial.sampled_operands);
|
||||
CHECK(op.preparation_report().physics_points == initial.physics_points);
|
||||
CHECK(*equation.calls == 2 * initial_calls);
|
||||
op.residual(f.workspace, after);
|
||||
op.prepare(values.state(f), values.u, f.workspace);
|
||||
op.residual(f.workspace, before);
|
||||
CHECK(relative_error(after, before) < 2.0e-15);
|
||||
|
||||
values.u *= 1.01;
|
||||
op.prepare(values.state(f), values.u, f.workspace, Mode::reuse_unchanged);
|
||||
CHECK(op.preparation_report().geometry_points == initial.geometry_points);
|
||||
CHECK(op.preparation_report().sampled_operands == 0);
|
||||
f.mesh.NodesUpdated();
|
||||
CHECK_THROWS_AS(op.residual(f.workspace, after), std::logic_error);
|
||||
op.prepare(values.state(f), values.u, f.workspace, Mode::reuse_unchanged);
|
||||
CHECK(op.preparation_report().geometry_points == initial.geometry_points);
|
||||
CHECK(op.preparation_report().sampled_operands == initial.sampled_operands);
|
||||
|
||||
values.h = -1.0;
|
||||
CHECK_FALSE(op.try_prepare(values.state(f), values.u, f.workspace, Mode::reuse_unchanged).has_value());
|
||||
CHECK_FALSE(op.is_prepared());
|
||||
CHECK_THROWS_AS(op.residual(f.workspace, after), std::logic_error);
|
||||
values.h = f.enthalpy();
|
||||
op.prepare(values.state(f), values.u, f.workspace, Mode::reuse_unchanged);
|
||||
CHECK(op.preparation_report().geometry_points == initial.geometry_points);
|
||||
CHECK(op.preparation_report().sampled_operands == initial.sampled_operands);
|
||||
op.invalidate();
|
||||
CHECK_FALSE(op.preparation_report().did_work());
|
||||
op.prepare(values.state(f), values.u, f.workspace, Mode::reuse_unchanged);
|
||||
CHECK(op.preparation_report().did_work());
|
||||
}
|
||||
|
||||
TEST_CASE("Selective preparation follows contribution dependencies and copied coefficient values", "[equations][serial_backend][unit]") {
|
||||
GravityFixture f;
|
||||
Values values(f);
|
||||
// Only the stellar density source reads density; the potential flux on every domain is reused.
|
||||
VolumeEquationFixture op{serif::equations::PrimalPoissonBalance{four_pi_G}, f.potential_binding, f.displacement_binding, f.compactification, f.mapper, reference_policy()};
|
||||
op.prepare(values.state(f), values.u, f.workspace);
|
||||
const auto initial = op.preparation_report();
|
||||
auto copied = values;
|
||||
op.prepare(copied.state(f), copied.u, f.workspace, Mode::reuse_unchanged);
|
||||
CHECK_FALSE(op.preparation_report().did_work());
|
||||
const auto elements = op.preparation_report().reused_elements;
|
||||
copied.rho *= 1.1;
|
||||
op.prepare(copied.state(f), copied.u, f.workspace, Mode::reuse_unchanged);
|
||||
CHECK(op.preparation_report().geometry_points == 0);
|
||||
CHECK(op.preparation_report().reused_elements > 0);
|
||||
CHECK(op.preparation_report().reused_elements < elements);
|
||||
CHECK(op.preparation_report().sampled_operands > 0);
|
||||
CHECK(op.preparation_report().sampled_operands < initial.sampled_operands);
|
||||
CHECK(op.preparation_report().physics_points > 0);
|
||||
CHECK(op.preparation_report().physics_points < initial.physics_points);
|
||||
Vector incremental, refreshed;
|
||||
op.residual(f.workspace, incremental);
|
||||
op.prepare(copied.state(f), copied.u, f.workspace);
|
||||
op.residual(f.workspace, refreshed);
|
||||
CHECK(relative_error(incremental, refreshed) < 2.0e-15);
|
||||
}
|
||||
|
||||
TEST_CASE("Selective preparation refreshes compactification without resampling unchanged operands", "[equations][serial_backend][unit]") {
|
||||
GravityFixture f;
|
||||
f.set_compactification();
|
||||
Values values(f);
|
||||
VolumeEquationFixture op{PotentialRow{four_pi_G}, f.potential_binding, f.displacement_binding, f.compactification, f.mapper, reference_policy()};
|
||||
op.prepare(values.state(f), values.u, f.workspace);
|
||||
const auto initial = op.preparation_report();
|
||||
Vector frozen, unchanged, incremental, refreshed, action, transpose;
|
||||
op.residual(f.workspace, frozen);
|
||||
f.compactification *= 0.97;
|
||||
op.residual(f.workspace, unchanged);
|
||||
CHECK(relative_error(unchanged, frozen) == 0.0);
|
||||
|
||||
op.prepare(values.state(f), values.u, f.workspace, Mode::reuse_unchanged);
|
||||
CHECK(op.preparation_report().geometry_points == initial.geometry_points);
|
||||
CHECK(op.preparation_report().sampled_operands == 0);
|
||||
op.residual(f.workspace, incremental);
|
||||
CHECK(relative_error(incremental, frozen) > 1.0e-8);
|
||||
const auto direction = pack(op, values.state(f), values.u);
|
||||
const auto dual = deterministic(op.height(), 0.31);
|
||||
op.apply_jacobian(direction, f.workspace, action);
|
||||
op.apply_jacobian_transpose(dual, f.workspace, transpose);
|
||||
|
||||
op.prepare(values.state(f), values.u, f.workspace);
|
||||
op.residual(f.workspace, refreshed);
|
||||
CHECK(relative_error(incremental, refreshed) < 2.0e-12);
|
||||
op.apply_jacobian(direction, f.workspace, refreshed);
|
||||
CHECK(relative_error(action, refreshed) < 2.0e-12);
|
||||
op.apply_jacobian_transpose(dual, f.workspace, refreshed);
|
||||
CHECK(relative_error(transpose, refreshed) < 2.0e-12);
|
||||
}
|
||||
|
||||
TEST_CASE("Dependency preparation detects coefficient binding replacement and ignores unused state roles", "[equations][serial_backend][unit]") {
|
||||
GravityFixture f;
|
||||
Values values(f);
|
||||
CountingClosure equation;
|
||||
VolumeEquationFixture op{equation, f.density_binding, f.displacement_binding, f.compactification, f.mapper, reference_policy()};
|
||||
op.prepare(values.state(f), values.u, f.workspace);
|
||||
const auto initial = op.preparation_report();
|
||||
const auto calls = *equation.calls;
|
||||
|
||||
values.omega[0] *= 1.2; // Rotation is present in the state but is not an input to closure.
|
||||
op.prepare(values.state(f), values.u, f.workspace, Mode::reuse_unchanged);
|
||||
CHECK_FALSE(op.preparation_report().did_work());
|
||||
CHECK(*equation.calls == calls);
|
||||
|
||||
discretization::backend::mfem::TermBinding<Density, Schema> replacement{f.density_space};
|
||||
const auto rebound = backend::make_term_coefficients(backend::bind(replacement, values.rho), backend::bind(f.enthalpy_binding, values.h));
|
||||
op.prepare(rebound, values.u, f.workspace, Mode::reuse_unchanged);
|
||||
CHECK(op.preparation_report().geometry_points == initial.geometry_points);
|
||||
CHECK(op.preparation_report().sampled_operands == initial.sampled_operands);
|
||||
CHECK(*equation.calls == 2 * calls);
|
||||
|
||||
Vector incremental, rebuilt;
|
||||
op.residual(f.workspace, incremental);
|
||||
op.prepare(rebound, values.u, f.workspace);
|
||||
op.residual(f.workspace, rebuilt);
|
||||
CHECK(relative_error(incremental, rebuilt) < 2.0e-15);
|
||||
}
|
||||
|
||||
TEST_CASE("STROID hanging nodes preserve complete volume actions and sparse field blocks", "[integration][serial_backend][nonconforming]") {
|
||||
const auto generated = serif::tests::nonconforming::mesh(1, 2);
|
||||
for (const auto ordering : {::mfem::Ordering::byNODES, ::mfem::Ordering::byVDIM}) {
|
||||
GravityFixture fixture(::mfem::Mesh(*generated.mesh), ordering);
|
||||
::mfem::GridFunctionCoefficient coordinate(generated.exterior_coordinate->values.get());
|
||||
fixture.compactification.ProjectCoefficient(coordinate);
|
||||
REQUIRE(fixture.potential_space.GetTrueVSize() < fixture.potential_space.GetVSize());
|
||||
check_operator(fixture, PotentialRow{four_pi_G}, fixture.potential_binding);
|
||||
Values values(fixture);
|
||||
VolumeEquationFixture equation{PotentialRow{four_pi_G}, fixture.potential_binding, fixture.displacement_binding,
|
||||
fixture.compactification, fixture.mapper, reference_policy()};
|
||||
equation.prepare(values.state(fixture), values.u, fixture.workspace);
|
||||
const auto matrix = backend::assemble_volume_field<Potential>(equation.data());
|
||||
const auto diagonal = backend::volume_field_diagonal<Potential>(equation.data());
|
||||
const Vector input = deterministic(fixture.potential_binding.size(), 0.7);
|
||||
Vector actual, expected(matrix->Height()), native_diagonal;
|
||||
auto direction = gravity_port::zeros(fixture);
|
||||
direction.potential = input;
|
||||
equation.apply_jacobian(fixture.state(direction.potential, direction.density), direction.displacement, fixture.workspace, actual);
|
||||
matrix->Mult(input, expected);
|
||||
CHECK(relative_error(actual, expected) < 3.0e-12);
|
||||
matrix->GetDiag(native_diagonal);
|
||||
for (int index = 0; index < native_diagonal.Size(); ++index) {
|
||||
CHECK(diagonal[index] == native_diagonal[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("STROID material hanging nodes preserve nonlinear density-balance and density-source actions", "[integration][serial_backend][nonconforming]") {
|
||||
const auto generated = serif::tests::nonconforming::mesh(0, 3);
|
||||
GravityFixture fixture(::mfem::Mesh(*generated.mesh), ::mfem::Ordering::byVDIM);
|
||||
::mfem::GridFunctionCoefficient coordinate(generated.exterior_coordinate->values.get());
|
||||
fixture.compactification.ProjectCoefficient(coordinate);
|
||||
require_material_hanging_coefficient(fixture.density_binding, fixture.density_binding);
|
||||
require_material_hanging_coefficient(fixture.displacement_binding, fixture.density_binding);
|
||||
|
||||
const serif::equations::BarotropicDensityBalance density_balance{eos::models::Polytrope{1.5, 0.25}};
|
||||
check_operator(fixture, density_balance, fixture.density_binding);
|
||||
check_operator(fixture, StellarDensitySource{four_pi_G}, fixture.potential_binding);
|
||||
|
||||
// Positive interior density keeps this nonlinear tangent away from the vacuum singularity.
|
||||
Values values(fixture), direction(fixture);
|
||||
direction.rho = deterministic(values.rho.Size(), 0.3);
|
||||
direction.phi = 0.0;
|
||||
direction.c = 0.0;
|
||||
direction.omega = 0.0;
|
||||
direction.u = fixture.displacement(0.001);
|
||||
VolumeEquationFixture density_equation{density_balance, fixture.density_binding, fixture.displacement_binding,
|
||||
fixture.compactification, fixture.mapper, reference_policy()};
|
||||
density_equation.prepare(values.state(fixture), values.u, fixture.workspace);
|
||||
const auto density_matrix = backend::assemble_volume_field<blocks::density::mass>(density_equation.data());
|
||||
const Vector zero_displacement = constant(values.u.Size(), 0.0);
|
||||
Vector actual, expected(density_matrix->Height());
|
||||
density_equation.apply_jacobian(direction.state(fixture), zero_displacement, fixture.workspace, actual);
|
||||
density_matrix->Mult(direction.rho, expected);
|
||||
CHECK(relative_error(actual, expected) < 3.0e-12);
|
||||
const Vector density_dual = deterministic(density_matrix->Height(), -0.8);
|
||||
Vector complete_transpose, sparse_transpose(density_matrix->Width());
|
||||
density_equation.apply_jacobian_transpose(density_dual, fixture.workspace, complete_transpose);
|
||||
density_matrix->MultTranspose(density_dual, sparse_transpose);
|
||||
const Vector field_transpose(complete_transpose.GetData(), direction.rho.Size());
|
||||
CHECK(relative_error(sparse_transpose, field_transpose) < 3.0e-12);
|
||||
|
||||
// The nonlinear residual independently checks the constrained field and geometry tangent together.
|
||||
density_equation.apply_jacobian(direction.state(fixture), direction.u, fixture.workspace, actual);
|
||||
constexpr double step = 1.0e-6;
|
||||
Values plus(values), minus(values);
|
||||
plus.rho.Add(step, direction.rho);
|
||||
minus.rho.Add(-step, direction.rho);
|
||||
plus.u.Add(step, direction.u);
|
||||
minus.u.Add(-step, direction.u);
|
||||
Vector positive, negative;
|
||||
density_equation.prepare(plus.state(fixture), plus.u, fixture.workspace);
|
||||
density_equation.residual(fixture.workspace, positive);
|
||||
density_equation.prepare(minus.state(fixture), minus.u, fixture.workspace);
|
||||
density_equation.residual(fixture.workspace, negative);
|
||||
positive -= negative;
|
||||
positive /= 2.0 * step;
|
||||
CHECK(relative_error(actual, positive) < 4.0e-8);
|
||||
|
||||
VolumeEquationFixture source_equation{StellarDensitySource{four_pi_G}, fixture.potential_binding,
|
||||
fixture.displacement_binding, fixture.compactification, fixture.mapper, reference_policy()};
|
||||
source_equation.prepare(values.state(fixture), values.u, fixture.workspace);
|
||||
const auto source_matrix = backend::assemble_volume_field<blocks::density::mass>(source_equation.data());
|
||||
ReferenceGeometry geometry{fixture, fixture.mapper, values.u};
|
||||
ReferenceVolume volume{geometry};
|
||||
::mfem::MixedBilinearForm native_source(&fixture.density_space, &fixture.potential_space);
|
||||
auto* integrator = new ::mfem::MixedScalarMassIntegrator(volume);
|
||||
const auto& rule = ::mfem::IntRules.Get(fixture.mesh.GetElementBaseGeometry(0), reference_order);
|
||||
integrator->SetIntRule(&rule);
|
||||
::mfem::Array<int> stellar{1, 1, 0};
|
||||
native_source.AddDomainIntegrator(integrator, stellar);
|
||||
native_source.Assemble();
|
||||
native_source.Finalize();
|
||||
|
||||
// Native local assembly and native P transpose retain all hanging-node cross terms.
|
||||
Vector native_full(fixture.potential_binding.full_size()), native_true(fixture.potential_binding.true_size());
|
||||
native_source.Mult(fixture.density_binding.scatter(values.rho), native_full);
|
||||
native_full *= four_pi_G;
|
||||
fixture.potential_space.GetConformingProlongation()->MultTranspose(native_full, native_true);
|
||||
const Vector native_residual = fixture.potential_binding.gather_true(native_true);
|
||||
source_equation.residual(fixture.workspace, actual);
|
||||
CHECK(relative_error(actual, native_residual) < 3.0e-12);
|
||||
expected.SetSize(source_matrix->Height());
|
||||
source_matrix->Mult(values.rho, expected);
|
||||
CHECK(relative_error(expected, native_residual) < 3.0e-12);
|
||||
|
||||
const Vector source_dual = deterministic(source_matrix->Height(), 0.7);
|
||||
native_full.SetSize(fixture.density_binding.full_size());
|
||||
native_true.SetSize(fixture.density_binding.true_size());
|
||||
native_source.MultTranspose(fixture.potential_binding.scatter(source_dual), native_full);
|
||||
native_full *= four_pi_G;
|
||||
fixture.density_space.GetConformingProlongation()->MultTranspose(native_full, native_true);
|
||||
const Vector native_transpose = fixture.density_binding.gather_true(native_true);
|
||||
sparse_transpose.SetSize(source_matrix->Width());
|
||||
source_matrix->MultTranspose(source_dual, sparse_transpose);
|
||||
CHECK(relative_error(sparse_transpose, native_transpose) < 3.0e-12);
|
||||
}
|
||||
252
tests/equations/state_validation.cpp
Normal file
252
tests/equations/state_validation.cpp
Normal file
@@ -0,0 +1,252 @@
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "serif/tests/equations/fixture.hpp"
|
||||
#include "serif/backend/mfem/discretization/element_coordinates.hpp"
|
||||
#include "serif/tests/test_tags.hpp"
|
||||
|
||||
namespace serif::tests::equations {
|
||||
TEST_CASE("Scalar Operand Evaluation Rejects Incompatible MFEM Elements", tags::unit & tags::equations) {
|
||||
Fixture f;
|
||||
::mfem::ND_FECollection collection(1, 3);
|
||||
::mfem::FiniteElementSpace space(&f.mesh, &collection);
|
||||
const auto& element = *space.GetFE(0);
|
||||
const auto& rule = ::mfem::IntRules.Get(element.GetGeomType(), 2);
|
||||
backend::BasisEvaluation basis;
|
||||
backend::TensorBasisCache cache;
|
||||
backend::BasisWorkspace workspace;
|
||||
auto& transformation = *f.mesh.GetElementTransformation(0);
|
||||
using Operand = discretization::forms::Operand<blocks::density::mass>;
|
||||
|
||||
// Direct sampling must enforce the scalar representation even without a TermBinding.
|
||||
CHECK_THROWS_AS(
|
||||
backend::OperandEvaluator<Operand>::sample_basis(element, rule, basis, cache, backend::ContractionMode::automatic, transformation, workspace),
|
||||
std::invalid_argument
|
||||
);
|
||||
}
|
||||
|
||||
TEST_CASE("Equation State Guards Remain Active For Invalid Sizes And Nonfinite Values", tags::unit & tags::equations) {
|
||||
Fixture f;
|
||||
auto rho = f.density(), h = f.enthalpy(), u = f.displacement(0.0);
|
||||
PreparedClosure prepared(
|
||||
Closure{eos::models::Polytrope{3.0, 1.5}}, f.density_binding, f.displacement_binding, f.compactification,
|
||||
f.mapper
|
||||
);
|
||||
::mfem::Vector result;
|
||||
CHECK_THROWS_AS(prepared.residual(f.workspace, result), std::logic_error);
|
||||
CHECK_THROWS_AS(prepared.width(), std::logic_error);
|
||||
const auto wrong = constant(rho.Size() + 1, 0.0);
|
||||
CHECK_THROWS_AS(prepared.prepare(f.state(wrong, h), u, f.workspace), std::invalid_argument);
|
||||
CHECK_FALSE(prepared.is_prepared());
|
||||
rho[0] = std::numeric_limits<double>::quiet_NaN();
|
||||
const auto invalid = prepared.try_prepare(f.state(rho, h), u, f.workspace);
|
||||
REQUIRE_FALSE(invalid.has_value());
|
||||
CHECK(invalid.error().reason == backend::PreparationRejectionReason::NON_FINITE);
|
||||
rho = f.density();
|
||||
prepared.prepare(f.state(rho, h), u, f.workspace);
|
||||
CHECK_THROWS_AS(prepared.apply_jacobian(f.state(wrong, h), u, f.workspace, result), std::invalid_argument);
|
||||
CHECK_THROWS_AS(prepared.apply_jacobian(wrong, f.workspace, result), std::invalid_argument);
|
||||
h[0] = std::numeric_limits<double>::infinity();
|
||||
CHECK_THROWS_AS(prepared.apply_jacobian(f.state(rho, h), u, f.workspace, result), std::domain_error);
|
||||
}
|
||||
|
||||
TEST_CASE("Equation Bindings Reject Wrong Spaces And Different Meshes", tags::unit & tags::equations) {
|
||||
Fixture f, other;
|
||||
::mfem::H1_FECollection wrong_collection(2, 3);
|
||||
::mfem::FiniteElementSpace wrong_order(&f.mesh, &wrong_collection);
|
||||
CHECK_THROWS_AS(
|
||||
(discretization::backend::mfem::TermBinding<::serif::tests::equations::Enthalpy, Schema>{wrong_order}), std::invalid_argument
|
||||
);
|
||||
CHECK_THROWS_AS(
|
||||
(discretization::backend::mfem::TermBinding<blocks::density::mass, Schema>{wrong_order}), std::invalid_argument
|
||||
);
|
||||
const auto rho = f.density(), h = other.enthalpy(), u = f.displacement(0.0);
|
||||
PreparedClosure prepared(
|
||||
Closure{eos::models::Polytrope{3.0, 1.5}}, f.density_binding, f.displacement_binding, f.compactification,
|
||||
f.mapper
|
||||
);
|
||||
const auto wrong_state =
|
||||
backend::make_term_coefficients(backend::bind(f.density_binding, rho), backend::bind(other.enthalpy_binding, h));
|
||||
CHECK_THROWS_AS(prepared.prepare(wrong_state, u, f.workspace), std::invalid_argument);
|
||||
const auto local_h = f.enthalpy();
|
||||
prepared.prepare(f.state(rho, local_h), u, f.workspace);
|
||||
::mfem::Vector result;
|
||||
CHECK_THROWS_AS(prepared.apply_jacobian(wrong_state, u, f.workspace, result), std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST_CASE("Prepared Equations Reject Changed Discretization And Domain Support", tags::unit & tags::equations) {
|
||||
Fixture f;
|
||||
const auto rho = f.density(), h = f.enthalpy(), u = f.displacement(0.0);
|
||||
PreparedClosure prepared(
|
||||
Closure{eos::models::Polytrope{3.0, 1.5}}, f.density_binding, f.displacement_binding, f.compactification,
|
||||
f.mapper
|
||||
);
|
||||
prepared.prepare(f.state(rho, h), u, f.workspace);
|
||||
::mfem::Vector result;
|
||||
SECTION("Changed domain attributes") {
|
||||
f.mesh.GetElement(0)->SetAttribute(3);
|
||||
CHECK_THROWS_AS(prepared.residual(f.workspace, result), std::logic_error);
|
||||
}
|
||||
SECTION("Mesh refinement") {
|
||||
f.mesh.UniformRefinement();
|
||||
CHECK_THROWS_AS(prepared.residual(f.workspace, result), std::logic_error);
|
||||
}
|
||||
SECTION("Updated node geometry") {
|
||||
f.mesh.NodesUpdated();
|
||||
CHECK_THROWS_AS(prepared.residual(f.workspace, result), std::logic_error);
|
||||
prepared.prepare(f.state(rho, h), u, f.workspace);
|
||||
CHECK_NOTHROW(prepared.residual(f.workspace, result));
|
||||
}
|
||||
SECTION("Rebound compactification") {
|
||||
Fixture other;
|
||||
f.compactification.SetSpace(&other.enthalpy_space);
|
||||
CHECK_THROWS_AS(prepared.residual(f.workspace, result), std::logic_error);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Equation Field Restriction Excludes Only Vacuum-Only Coefficients", tags::unit & tags::equations) {
|
||||
Fixture f;
|
||||
auto full = constant(f.density_space.GetVSize(), 0.0);
|
||||
::mfem::Array<int> dofs;
|
||||
std::vector<bool> stellar_dofs(full.Size(), false);
|
||||
for (int element = 0; element < f.mesh.GetNE(); ++element) {
|
||||
if (f.mesh.GetAttribute(element) == 3) {
|
||||
continue;
|
||||
}
|
||||
f.density_space.GetElementDofs(element, dofs);
|
||||
for (const int dof : dofs) {
|
||||
stellar_dofs[dof] = true;
|
||||
}
|
||||
}
|
||||
for (int dof = 0; dof < full.Size(); ++dof) {
|
||||
full[dof] = stellar_dofs[dof] ? 0.5 : std::numeric_limits<double>::quiet_NaN();
|
||||
}
|
||||
const auto selected = std::count(stellar_dofs.begin(), stellar_dofs.end(), true);
|
||||
REQUIRE(selected > 0);
|
||||
REQUIRE(selected < full.Size());
|
||||
CHECK(f.density_binding.size() == selected);
|
||||
const auto reduced = f.density_binding.gather(full);
|
||||
CHECK_NOTHROW(f.density_binding.validate_values(reduced));
|
||||
const auto expanded = f.density_binding.scatter(reduced);
|
||||
CHECK(expanded.CheckFinite() == 0);
|
||||
CHECK(reduced.Min() == 0.5);
|
||||
}
|
||||
TEST_CASE("Equation Bindings Reject Unsupported Meshes And Element Indices", tags::unit & tags::equations) {
|
||||
Fixture f;
|
||||
CHECK_THROWS_AS(f.density_binding.supports(-1), std::out_of_range);
|
||||
CHECK_THROWS_AS(f.density_binding.supports(f.mesh.GetNE()), std::out_of_range);
|
||||
::mfem::Vector empty, output;
|
||||
CHECK_THROWS_AS(f.density_binding.element_values(0, empty, output), std::invalid_argument);
|
||||
auto flat_mesh = ::mfem::Mesh::MakeCartesian2D(2, 2, ::mfem::Element::QUADRILATERAL);
|
||||
::mfem::L2_FECollection flat_collection(2, 2);
|
||||
::mfem::FiniteElementSpace flat_space(&flat_mesh, &flat_collection);
|
||||
CHECK_THROWS_AS((discretization::backend::mfem::TermBinding<blocks::density::mass, Schema>{flat_space}), std::invalid_argument);
|
||||
auto nonconforming_mesh = make_mesh(::mfem::Element::HEXAHEDRON);
|
||||
nonconforming_mesh.EnsureNCMesh();
|
||||
::mfem::FiniteElementSpace nonconforming_space(&nonconforming_mesh, &f.density_collection);
|
||||
CHECK_NOTHROW((discretization::backend::mfem::TermBinding<blocks::density::mass, Schema>{nonconforming_space}));
|
||||
}
|
||||
namespace {
|
||||
/** @brief Compare primal/dual coordinate access with independent MFEM extraction and assembly. */
|
||||
void check_element_coordinates(const discretization::backend::mfem::DofMap& binding) {
|
||||
const auto reduced = deterministic(binding.size(), 0.31);
|
||||
const auto full = binding.scatter(reduced);
|
||||
const auto elements = binding.validated_elements();
|
||||
for (int element = 0; element < binding.mesh().GetNE(); ++element) {
|
||||
::mfem::Array<int> dofs;
|
||||
::mfem::DofTransformation transformation;
|
||||
binding.space().GetElementVDofs(element, dofs, transformation);
|
||||
::mfem::Vector expected, actual;
|
||||
full.GetSubVector(dofs, expected);
|
||||
if (transformation.GetDofTransformation() != nullptr) {
|
||||
transformation.InvTransformPrimal(expected);
|
||||
}
|
||||
elements.element_values(element, reduced, actual);
|
||||
CHECK(relative_error(actual, expected) < 1.0e-13);
|
||||
|
||||
const auto coordinates = elements.coordinates(element);
|
||||
REQUIRE(coordinates.size() == dofs.Size());
|
||||
for (int local = 0; local < dofs.Size(); ++local) {
|
||||
const int full_index = dofs[local] < 0 ? -1 - dofs[local] : dofs[local];
|
||||
const int index = binding.reduced_dof(full_index).value_or(-1);
|
||||
CHECK(coordinates.reduced_dof(local) == index);
|
||||
if (index >= 0) {
|
||||
CHECK(coordinates.local_dof(index) == local);
|
||||
::mfem::Vector unit = constant(binding.size(), 0.0), direction;
|
||||
unit[index] = 1.0;
|
||||
coordinates.element_values(unit, expected);
|
||||
coordinates.unit_direction(local, direction);
|
||||
CHECK(relative_error(direction, expected) < 1.0e-13);
|
||||
}
|
||||
}
|
||||
|
||||
const auto dual = deterministic(dofs.Size(), 0.78);
|
||||
::mfem::Vector expected_dual(dual), actual_dual(dual);
|
||||
auto expected_full = constant(binding.full_size(), 0.0);
|
||||
auto assembled = constant(binding.full_size(), 0.0);
|
||||
if (transformation.GetDofTransformation() != nullptr) {
|
||||
transformation.TransformDual(expected_dual);
|
||||
}
|
||||
expected_full.AddElementVector(dofs, expected_dual);
|
||||
elements.add_element(element, actual_dual, assembled);
|
||||
CHECK(relative_error(assembled, expected_full) < 1.0e-13);
|
||||
CHECK(full * assembled == Catch::Approx(actual * dual).margin(1.0e-12));
|
||||
|
||||
actual_dual = dual;
|
||||
coordinates.orient_dual(actual_dual);
|
||||
for (int local = 0; local < dofs.Size(); ++local) {
|
||||
const double expected_sign = dofs[local] < 0 ? -1.0 : 1.0;
|
||||
CHECK(actual_dual[local] == Catch::Approx(expected_sign * expected_dual[local]).margin(1.0e-14));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Validated Element Traversals Preserve Native MFEM Coordinates", tags::unit & tags::equations) {
|
||||
for (const auto geometry : {::mfem::Element::HEXAHEDRON, ::mfem::Element::TETRAHEDRON}) {
|
||||
for (const auto ordering : {::mfem::Ordering::byNODES, ::mfem::Ordering::byVDIM}) {
|
||||
Fixture fixture(geometry, ordering);
|
||||
check_element_coordinates(fixture.density_binding);
|
||||
check_element_coordinates(fixture.enthalpy_binding);
|
||||
check_element_coordinates(fixture.displacement_binding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Validated Element Traversals Preserve Global Scalar Support And Guards", tags::unit & tags::equations) {
|
||||
Fixture fixture;
|
||||
const auto& binding = fixture.constant_binding;
|
||||
const auto elements = binding.validated_elements();
|
||||
const auto reduced = constant(1, 2.0);
|
||||
for (int element = 0; element < fixture.mesh.GetNE(); ++element) {
|
||||
::mfem::Vector local;
|
||||
elements.element_values(element, reduced, local);
|
||||
CHECK(local[0] == (binding.supports(element) ? 2.0 : 0.0));
|
||||
auto assembled = constant(1, 0.0);
|
||||
auto dual = constant(1, 3.0);
|
||||
elements.add_element(element, dual, assembled);
|
||||
CHECK(assembled[0] == (binding.supports(element) ? 3.0 : 0.0));
|
||||
}
|
||||
::mfem::Vector output, empty;
|
||||
CHECK_THROWS_AS(elements.element_values(-1, reduced, output), std::out_of_range);
|
||||
CHECK_THROWS_AS(elements.element_values(0, empty, output), std::invalid_argument);
|
||||
CHECK_THROWS_AS(elements.add_element(0, empty, output), std::invalid_argument);
|
||||
fixture.mesh.GetElement(0)->SetAttribute(3);
|
||||
CHECK_THROWS_AS(binding.validated_elements(), std::logic_error);
|
||||
CHECK_THROWS_AS(binding.element_values(1, reduced, output), std::logic_error);
|
||||
CHECK_THROWS_AS(elements.element_values(0, reduced, output), std::logic_error);
|
||||
}
|
||||
|
||||
TEST_CASE("Validated Element Traversals Reject Changed Mesh Revisions", tags::unit & tags::equations) {
|
||||
Fixture fixture;
|
||||
const auto elements = fixture.enthalpy_binding.validated_elements();
|
||||
const auto values = fixture.enthalpy();
|
||||
fixture.mesh.UniformRefinement();
|
||||
::mfem::Vector output;
|
||||
CHECK_THROWS_AS(elements.element_values(0, values, output), std::logic_error);
|
||||
CHECK_THROWS_AS(fixture.enthalpy_binding.validated_elements(), std::logic_error);
|
||||
}
|
||||
} // namespace serif::tests::equations
|
||||
93
tests/equations/surface_potential_test.cpp
Normal file
93
tests/equations/surface_potential_test.cpp
Normal file
@@ -0,0 +1,93 @@
|
||||
#include <cmath>
|
||||
#include <numbers>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "serif/tests/deformation/mesh.hpp"
|
||||
#include "serif/backend/mfem/model/surface_potential.hpp"
|
||||
#include "serif/backend/mfem/discretization/term_binding.hpp"
|
||||
#include "serif/backend/mfem/geometry/deformation/reference_geometry.hpp"
|
||||
|
||||
namespace {
|
||||
namespace deformation = serif::geometry::deformation;
|
||||
namespace backend = serif::model::backend::mfem;
|
||||
using Potential = serif::discretization::blocks::gravity::potential;
|
||||
|
||||
/** @brief Compare owned surface matrices with a coupled finite difference of the physical moments. */
|
||||
void check_surface(const int surface_order, const bool nodal) {
|
||||
serif::tests::deformation::PairedMesh mesh(0, 0.1, surface_order);
|
||||
::mfem::H1_FECollection potential_collection(Potential::FunctionalSpace::order, 3);
|
||||
::mfem::FiniteElementSpace potential_space(mesh.generated.mesh.get(), &potential_collection);
|
||||
serif::discretization::backend::mfem::TermBinding<Potential, serif::tests::deformation::Schema> potential_binding(potential_space);
|
||||
const auto reference = deformation::backend::mfem::sample_reference_surface(mesh.scalar_space, mesh.surface_attribute);
|
||||
REQUIRE(reference.has_value());
|
||||
const auto embedding = [&] {
|
||||
if (nodal) {
|
||||
return deformation::build_surface_embedding(deformation::NodalRadialSurface{}, reference->positions);
|
||||
}
|
||||
return deformation::build_surface_embedding(deformation::SphericalHarmonicSurface{.maximum_degree = 1}, reference->positions);
|
||||
}();
|
||||
REQUIRE(embedding.has_value());
|
||||
const deformation::backend::mfem::SurfaceDiscretization discretization{
|
||||
.scalar_space = mesh.scalar_space, .surface_attribute = mesh.surface_attribute};
|
||||
const serif::equations::SurfacePotentialBalance equation{{1.0, 2.0, 3.0}, {0.1, -0.2, 0.3}};
|
||||
std::vector<double> shape(embedding->input_size()), displacement(embedding->output_size());
|
||||
::mfem::Vector potential(potential_binding.size()), potential_direction(potential.Size()), shape_direction(static_cast<int>(shape.size()));
|
||||
for (int index = 0; index < potential.Size(); ++index) {
|
||||
potential[index] = -1.0 + 0.02 * std::sin(index + 0.3);
|
||||
potential_direction[index] = std::cos(index + 0.2);
|
||||
}
|
||||
for (int index = 0; index < shape_direction.Size(); ++index) {
|
||||
shape[index] = 0.01 * std::sin(index + 0.7);
|
||||
shape_direction[index] = 0.03 * std::cos(index + 0.4);
|
||||
}
|
||||
REQUIRE(embedding->apply(shape, displacement).has_value());
|
||||
const auto accepted = backend::assemble_surface_potential(equation, discretization, *reference, *embedding,
|
||||
displacement, potential_binding, potential, -1.2, 0.4);
|
||||
::mfem::Vector action(shape_direction.Size()), temporary(action.Size());
|
||||
accepted.potential->Mult(potential_direction, action);
|
||||
accepted.shape->Mult(shape_direction, temporary);
|
||||
action += temporary;
|
||||
for (int row = 0; row < action.Size(); ++row) {
|
||||
action[row] += 0.2 * (*accepted.level)(row, 0) - 0.3 * (*accepted.angular_velocity)(row, 0);
|
||||
}
|
||||
constexpr double step = 2.0e-6;
|
||||
auto plus_potential = potential, minus_potential = potential;
|
||||
plus_potential.Add(step, potential_direction);
|
||||
minus_potential.Add(-step, potential_direction);
|
||||
auto plus_shape = shape, minus_shape = shape;
|
||||
for (int index = 0; index < shape_direction.Size(); ++index) {
|
||||
plus_shape[index] += step * shape_direction[index];
|
||||
minus_shape[index] -= step * shape_direction[index];
|
||||
}
|
||||
REQUIRE(embedding->apply(plus_shape, displacement).has_value());
|
||||
const auto plus = backend::assemble_surface_potential(equation, discretization, *reference, *embedding,
|
||||
displacement, potential_binding, plus_potential, -1.2 + step * 0.2, 0.4 - step * 0.3);
|
||||
REQUIRE(embedding->apply(minus_shape, displacement).has_value());
|
||||
const auto minus = backend::assemble_surface_potential(equation, discretization, *reference, *embedding,
|
||||
displacement, potential_binding, minus_potential, -1.2 - step * 0.2, 0.4 + step * 0.3);
|
||||
for (int row = 0; row < action.Size(); ++row) {
|
||||
CHECK(std::abs(action[row] - (plus.residual[row] - minus.residual[row]) / (2.0 * step)) < 2.0e-9);
|
||||
}
|
||||
|
||||
// Constant potential and zero rotation give a known normalized moment, independent of surface area.
|
||||
potential = -0.8;
|
||||
const auto constant = backend::assemble_surface_potential(equation, discretization, *reference, *embedding,
|
||||
displacement, potential_binding, potential, -1.2, 0.0);
|
||||
double moment = 0.0;
|
||||
if (nodal) {
|
||||
for (const double value : constant.residual) { moment += value; }
|
||||
CHECK(std::abs(moment - 0.4) < 3.0e-12);
|
||||
} else {
|
||||
CHECK(std::abs(constant.residual.front() - 0.4 / std::sqrt(4.0 * std::numbers::pi)) < 3.0e-12);
|
||||
}
|
||||
constant.shape->Mult(shape_direction, temporary);
|
||||
CHECK(temporary.Norml2() == 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Isobar potential moments preserve constant mode and differentiate pullback geometry", "[model][surface_potential][unit]") {
|
||||
for (const int surface_order : {2, 3}) {
|
||||
check_surface(surface_order, false);
|
||||
check_surface(surface_order, true);
|
||||
}
|
||||
}
|
||||
248
tests/equations/tensor_basis.cpp
Normal file
248
tests/equations/tensor_basis.cpp
Normal file
@@ -0,0 +1,248 @@
|
||||
#include <cmath>
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include "mfem/fem/fe_coll.hpp"
|
||||
#include "serif/backend/mfem/discretization/basis_evaluation.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace serif::discretization::backend::mfem;
|
||||
|
||||
/// Compare prepared tensor or mapped actions against independently sampled dense matrices.
|
||||
void check_actions(const BasisEvaluation& tensor, const BasisEvaluation& dense, const double tolerance) {
|
||||
::mfem::Vector dofs(tensor.width()), dual(tensor.height()), values, reference, assembled, reference_transpose;
|
||||
for (int i = 0; i < dofs.Size(); ++i) {
|
||||
dofs[i] = std::sin(0.53 * (i + 1));
|
||||
}
|
||||
for (int i = 0; i < dual.Size(); ++i) {
|
||||
dual[i] = std::cos(0.31 * (i + 1));
|
||||
}
|
||||
BasisWorkspace workspace;
|
||||
for (int repeat = 0; repeat < 2; ++repeat) {
|
||||
if (repeat) {
|
||||
dofs = 0.0;
|
||||
dual = 0.0;
|
||||
}
|
||||
tensor.evaluate(dofs, values, workspace);
|
||||
dense.evaluate(dofs, reference, workspace);
|
||||
::mfem::Vector error = values;
|
||||
error -= reference;
|
||||
CHECK(error.Normlinf() <= tolerance * std::max(1.0, reference.Normlinf()));
|
||||
tensor.integrate(dual, assembled, workspace);
|
||||
dense.integrate(dual, reference_transpose, workspace);
|
||||
error = assembled;
|
||||
error -= reference_transpose;
|
||||
CHECK(error.Norml2() <= tolerance * std::max(1.0, reference_transpose.Norml2()));
|
||||
CHECK(std::abs(values * dual - dofs * assembled) <= tolerance * std::max(1.0, std::abs(values * dual)));
|
||||
if (repeat) {
|
||||
CHECK(values.Norml2() == 0.0);
|
||||
CHECK(assembled.Norml2() == 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare independent native MFEM basis matrices with the factored contractions and their transposes.
|
||||
void check_basis(const ::mfem::FiniteElement& element, const ::mfem::IntegrationRule& rule, ReferenceOperation operation, int copies, double tolerance) {
|
||||
TensorBasisCache cache;
|
||||
BasisEvaluation tensor, dense;
|
||||
tensor.sample(element, rule, operation, copies, cache, ContractionMode::automatic);
|
||||
dense.sample(element, rule, operation, copies, cache, ContractionMode::dense);
|
||||
REQUIRE(tensor.uses_tensor());
|
||||
REQUIRE_FALSE(dense.uses_tensor());
|
||||
check_actions(tensor, dense, tolerance);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TEST_CASE("Scalar and vector H1 tensor values gradients and transposes retain MeanField accuracy", "[equations][tensor_basis][unit]")
|
||||
{
|
||||
// MeanField tests/fem/reference_tables.cpp: retain its orders, bases and thresholds.
|
||||
for (const int order : {1, 2, 4})
|
||||
{
|
||||
for (const int basis : {::mfem::BasisType::GaussLobatto, ::mfem::BasisType::ClosedUniform})
|
||||
{
|
||||
::mfem::H1_FECollection h1(order, 3, basis);
|
||||
const auto& element = *h1.FiniteElementForGeometry(::mfem::Geometry::CUBE);
|
||||
const auto& rule = ::mfem::IntRules.Get(::mfem::Geometry::CUBE, 2 * order + 3);
|
||||
for (const int copies : {1, 2, 3})
|
||||
{
|
||||
check_basis(element, rule, ReferenceOperation::value, copies, 2.0e-12);
|
||||
check_basis(element, rule, ReferenceOperation::gradient, copies, 2.0e-11);
|
||||
check_basis(element, rule, ReferenceOperation::value_and_gradient, copies, 2.0e-12);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const int order : {0, 1, 3})
|
||||
{
|
||||
for (const int basis : {::mfem::BasisType::GaussLegendre, ::mfem::BasisType::OpenUniform})
|
||||
{
|
||||
::mfem::L2_FECollection l2(order, 3, basis);
|
||||
const auto& element = *l2.FiniteElementForGeometry(::mfem::Geometry::CUBE);
|
||||
const auto& rule = ::mfem::IntRules.Get(::mfem::Geometry::CUBE, 2 * order + 3);
|
||||
for (const int copies : {1, 2, 3})
|
||||
{
|
||||
check_basis(element, rule, ReferenceOperation::value, copies, 2.0e-12);
|
||||
check_basis(element, rule, ReferenceOperation::gradient, copies, 2.0e-11);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Tensor plans own their factors and reject non Cartesian rules without changing quadrature", "[equations][tensor_basis][unit]")
|
||||
{
|
||||
::mfem::H1_FECollection h1(3, 3);
|
||||
const auto& element = *h1.FiniteElementForGeometry(::mfem::Geometry::CUBE);
|
||||
const auto& original = ::mfem::IntRules.Get(::mfem::Geometry::CUBE, 9);
|
||||
::mfem::IntegrationRule copy(original.GetNPoints());
|
||||
for (int q = 0; q < copy.GetNPoints(); ++q)
|
||||
{
|
||||
copy.IntPoint(q) = original.IntPoint(q);
|
||||
}
|
||||
TensorBasisCache cache;
|
||||
auto retained = cache.get(element, copy, ReferenceOperation::value, 1);
|
||||
REQUIRE(retained);
|
||||
CHECK(cache.get(element, original, ReferenceOperation::value, 1) == retained);
|
||||
CHECK(cache.size() == 1);
|
||||
copy.IntPoint(copy.GetNPoints() - 1).x += 0.017;
|
||||
CHECK_FALSE(cache.get(element, copy, ReferenceOperation::value, 1));
|
||||
CHECK(cache.size() == 2);
|
||||
cache.clear();
|
||||
CHECK(cache.size() == 0);
|
||||
::mfem::Vector dofs(retained->dofs()), values;
|
||||
dofs = 1.0;
|
||||
TensorWorkspace workspace;
|
||||
retained->apply(dofs, values, workspace);
|
||||
for (int q = 0; q < values.Size(); ++q)
|
||||
{
|
||||
CHECK(std::abs(values[q] - 1.0) < 2.0e-12);
|
||||
}
|
||||
|
||||
const auto& tetrahedron = *h1.FiniteElementForGeometry(::mfem::Geometry::TETRAHEDRON);
|
||||
const auto& tetrahedron_rule = ::mfem::IntRules.Get(::mfem::Geometry::TETRAHEDRON, 9);
|
||||
CHECK_FALSE(TensorBasis::make(tetrahedron, tetrahedron_rule, ReferenceOperation::value, 1));
|
||||
CHECK_FALSE(TensorBasis::make(element, original, ReferenceOperation::value, 0));
|
||||
BasisEvaluation fallback;
|
||||
fallback.sample(element, copy, ReferenceOperation::value, 1, cache, ContractionMode::automatic);
|
||||
CHECK_FALSE(fallback.uses_tensor());
|
||||
}
|
||||
|
||||
TEST_CASE("Reused basis actions retain mappings through composition resampling and dense fallback", "[equations][tensor_basis][unit]") {
|
||||
::mfem::H1_FECollection h1(3, 3);
|
||||
const auto& element = *h1.FiniteElementForGeometry(::mfem::Geometry::CUBE);
|
||||
const auto& rule = ::mfem::IntRules.Get(::mfem::Geometry::CUBE, 9);
|
||||
TensorBasisCache cache;
|
||||
BasisEvaluation reference, dense_reference;
|
||||
reference.sample(element, rule, ReferenceOperation::value_and_gradient, 3, cache, ContractionMode::automatic);
|
||||
dense_reference.sample(element, rule, ReferenceOperation::value_and_gradient, 3, cache, ContractionMode::dense);
|
||||
BasisEvaluation tensor = reference;
|
||||
BasisEvaluation dense = dense_reference;
|
||||
BasisWorkspace workspace;
|
||||
|
||||
// Direct mapping of a sampled basis must disable its canonical shortcut, even without reset_from.
|
||||
for (int point = 0; point < rule.GetNPoints(); ++point) {
|
||||
const auto map = [point](const ::mfem::Vector& input, ::mfem::Vector& output) {
|
||||
for (int component = 0; component < output.Size(); ++component) {
|
||||
output[component] = (1.0 + 0.01 * point) * input[component] - 0.2 * input[(component + 1) % input.Size()];
|
||||
}
|
||||
};
|
||||
tensor.map_point(point, reference, map, workspace);
|
||||
dense.map_point(point, dense_reference, map, workspace);
|
||||
}
|
||||
check_actions(tensor, dense, 2.0e-12);
|
||||
|
||||
// A mapped reference remains usable when the next physical map changes component count.
|
||||
reference = tensor;
|
||||
dense_reference = dense;
|
||||
tensor.reset_from(reference, 5);
|
||||
dense.reset_from(dense_reference, 5);
|
||||
for (int point = 0; point < rule.GetNPoints(); ++point) {
|
||||
const auto map = [](const ::mfem::Vector& input, ::mfem::Vector& output) {
|
||||
for (int component = 0; component < output.Size(); ++component) {
|
||||
output[component] = 0.7 * input[component] + 0.4 * input[component + 5];
|
||||
}
|
||||
};
|
||||
tensor.map_point(point, reference, map, workspace);
|
||||
dense.map_point(point, dense_reference, map, workspace);
|
||||
}
|
||||
check_actions(tensor, dense, 2.0e-12);
|
||||
|
||||
// Reusing the same object for canonical, dense and constant bases must clear the earlier representation.
|
||||
for (const ContractionMode mode : {ContractionMode::automatic, ContractionMode::dense, ContractionMode::automatic}) {
|
||||
tensor.sample(element, rule, ReferenceOperation::gradient, 2, cache, mode);
|
||||
dense.sample(element, rule, ReferenceOperation::gradient, 2, cache, ContractionMode::dense);
|
||||
check_actions(tensor, dense, 2.0e-11);
|
||||
}
|
||||
tensor.set_constant(rule.GetNPoints());
|
||||
dense.set_constant(rule.GetNPoints());
|
||||
check_actions(tensor, dense, 2.0e-12);
|
||||
}
|
||||
|
||||
TEST_CASE("Canonical mapped and dense basis actions preserve aliased inputs while resizing", "[equations][tensor_basis][unit]") {
|
||||
::mfem::H1_FECollection h1(2, 3);
|
||||
const auto& element = *h1.FiniteElementForGeometry(::mfem::Geometry::CUBE);
|
||||
TensorBasisCache cache;
|
||||
BasisWorkspace workspace;
|
||||
for (const int order : {5, 7}) {
|
||||
const auto& rule = ::mfem::IntRules.Get(::mfem::Geometry::CUBE, order);
|
||||
for (const auto mode : {ContractionMode::automatic, ContractionMode::dense}) {
|
||||
CAPTURE(order, mode);
|
||||
BasisEvaluation reference, dense_reference;
|
||||
reference.sample(element, rule, ReferenceOperation::value, 1, cache, mode);
|
||||
dense_reference.sample(element, rule, ReferenceOperation::value, 1, cache, ContractionMode::dense);
|
||||
::mfem::Vector input(reference.width()), dual(reference.height()), expected, aliased;
|
||||
for (int index = 0; index < input.Size(); ++index) {
|
||||
input[index] = std::sin(0.53 * (index + 1));
|
||||
}
|
||||
for (int index = 0; index < dual.Size(); ++index) {
|
||||
dual[index] = std::cos(0.31 * (index + 1));
|
||||
}
|
||||
for (const bool mapped : {false, true}) {
|
||||
BasisEvaluation basis = reference;
|
||||
BasisEvaluation dense = dense_reference;
|
||||
if (mapped) {
|
||||
for (int point = 0; point < rule.GetNPoints(); ++point) {
|
||||
const auto map = [point](const ::mfem::Vector& source, ::mfem::Vector& destination) {
|
||||
destination[0] = (1.0 + 0.03 * point) * source[0];
|
||||
};
|
||||
basis.map_point(point, reference, map, workspace);
|
||||
dense.map_point(point, dense_reference, map, workspace);
|
||||
}
|
||||
}
|
||||
dense.evaluate(input, expected, workspace);
|
||||
aliased = input;
|
||||
basis.evaluate(aliased, aliased, workspace);
|
||||
aliased -= expected;
|
||||
CHECK(aliased.Normlinf() <= 2.0e-12);
|
||||
dense.integrate(dual, expected, workspace);
|
||||
aliased = dual;
|
||||
basis.integrate(aliased, aliased, workspace);
|
||||
aliased -= expected;
|
||||
CHECK(aliased.Normlinf() <= 2.0e-12);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Implicit canonical identities preserve untouched points and support in place maps", "[equations][tensor_basis][unit]") {
|
||||
::mfem::H1_FECollection h1(2, 3);
|
||||
const auto& element = *h1.FiniteElementForGeometry(::mfem::Geometry::CUBE);
|
||||
const auto& rule = ::mfem::IntRules.Get(::mfem::Geometry::CUBE, 7);
|
||||
TensorBasisCache cache;
|
||||
BasisWorkspace workspace;
|
||||
BasisEvaluation tensor, dense;
|
||||
// More than twelve components ensures mapping scratch does not restrict generality.
|
||||
tensor.sample(element, rule, ReferenceOperation::value_and_gradient, 4, cache, ContractionMode::automatic);
|
||||
dense.sample(element, rule, ReferenceOperation::value_and_gradient, 4, cache, ContractionMode::dense);
|
||||
REQUIRE(tensor.uses_tensor());
|
||||
const auto map = [](const ::mfem::Vector& input, ::mfem::Vector& output) {
|
||||
for (int component = 0; component < output.Size(); ++component) {
|
||||
output[component] = 0.8 * input[component] + 0.3 * input[(component + 1) % input.Size()];
|
||||
}
|
||||
};
|
||||
// A first map materializes only the destination representation; later maps compose with it.
|
||||
for (const int point : {3, 0, 3}) {
|
||||
tensor.map_point(point, tensor, map, workspace);
|
||||
dense.map_point(point, dense, map, workspace);
|
||||
check_actions(tensor, dense, 2.0e-11);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user