Files
FESADev/tests/unit/analysis/hht_shell_physics_test.cpp
T

3667 lines
176 KiB
C++

#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <cfenv>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <initializer_list>
#include <iostream>
#include <limits>
#include <memory>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
#include "analysis/hht_dynamic_internal.h"
#include "analysis/hht_reduced_internal.h"
#include "analysis/hht_reduced_increment.h"
#include "analysis/hht_reduced_procedure.h"
#include "fesa/analysis/analysis_model.h"
#include "fesa/analysis/newton_iteration_log.h"
#include "fesa/assembly/parallel_for.h"
#include "fesa/elements/finite_rotation.h"
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
#include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h"
namespace {
using Frame = fesa::DynamicAcceptedFrameState;
using Assembly = fesa::hht_internal::DynamicAssembly;
using L3 = std::array<long double, 3>;
constexpr double kEndTime = 0.25;
std::string Message(const fesa::Status& status) {
return status.Diagnostics().empty()
? "Unspecified solver failure"
: status.Diagnostics().front().code + ": " +
status.Diagnostics().front().message;
}
void Require(const fesa::Status& status) {
if (!status.IsOk()) throw std::runtime_error{Message(status)};
}
template <typename T>
T Require(fesa::Result<T> result) {
if (!result.HasValue()) throw std::runtime_error{Message(result.GetStatus())};
return std::move(result.Value());
}
L3 Add(const L3& a, const L3& b) {
return {a[0] + b[0], a[1] + b[1], a[2] + b[2]};
}
L3 Scale(const L3& a, const long double factor) {
return {factor * a[0], factor * a[1], factor * a[2]};
}
L3 Cross(const L3& a, const L3& b) {
return {a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0]};
}
long double Dot(const L3& a, const L3& b) {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}
long double Norm(const L3& a) {
return std::hypot(a[0], std::hypot(a[1], a[2]));
}
L3 Triple(const fesa::Vector& vector, const std::size_t offset) {
return {vector[offset], vector[offset + 1U], vector[offset + 2U]};
}
L3 Coordinates(const fesa::Node& node) {
return {node.coordinates[0], node.coordinates[1], node.coordinates[2]};
}
void Store(const L3& value, fesa::Vector* vector, const std::size_t offset) {
for (std::size_t c = 0; c < 3; ++c)
(*vector)[offset + c] = static_cast<double>(value[c]);
}
// Independent subset-block exponential, with no Rodrigues/Jl scalar formulas.
// The final block gives the mixed derivative in the supplied chart directions.
L3 ExponentialDerivative(const L3& phi, const L3& initial,
const std::vector<L3>& directions = {}) {
std::array<L3, 16> term{};
std::array<L3, 16> sum{};
term[0] = sum[0] = initial;
const std::size_t blocks = std::size_t{1} << directions.size();
for (std::size_t order = 1U; order <= 64U; ++order) {
std::array<L3, 16> next{};
for (std::size_t subset = 0; subset < blocks; ++subset) {
next[subset] = Cross(phi, term[subset]);
for (std::size_t direction = 0; direction < directions.size();
++direction) {
const std::size_t bit = std::size_t{1} << direction;
if ((subset & bit) != 0U)
next[subset] = Add(next[subset],
Cross(directions[direction], term[subset ^ bit]));
}
next[subset] =
Scale(next[subset], 1.0L / static_cast<long double>(order));
sum[subset] = Add(sum[subset], next[subset]);
}
term = next;
}
return sum[blocks - 1U];
}
struct Fixture {
std::unique_ptr<fesa::Domain> domain;
fesa::DofManager dofs;
std::vector<fesa::hht_internal::RuntimeShell> shells;
double length;
};
Fixture MakeFixture(const std::size_t divisions,
const bool lifecycle_translation = false) {
fesa::ModelDefinition definition{};
definition.source_path = "hht-shell-physics.inp";
definition.source_content_identity = "internal-C08-fixture";
for (std::size_t y = 0; y <= divisions; ++y) {
for (std::size_t x = 0; x <= divisions; ++x) {
const auto index = definition.nodes.size();
const auto label = static_cast<std::int64_t>(index + 1U);
definition.nodes.push_back({{"Plate", label, std::to_string(label)},
{-1.0 + 2.0 * static_cast<double>(x) /
static_cast<double>(divisions),
-1.0 + 2.0 * static_cast<double>(y) /
static_cast<double>(divisions),
0.0},
{}});
definition.shell_node_initial_frames.push_back(
{static_cast<fesa::EntityIndex>(index),
{0.0, 0.0, 1.0},
{1.0, 0.0, 0.0},
{0.0, 1.0, 0.0}});
}
}
definition.materials = {{"Elastic", 1000.0, 0.3, {}}};
definition.densities = {{0U, 1.0, {}}};
definition.shell_sections = {{"Centered", 0.2, 0U, {}}};
for (std::size_t y = 0; y < divisions; ++y) {
for (std::size_t x = 0; x < divisions; ++x) {
const auto first =
static_cast<fesa::EntityIndex>(y * (divisions + 1U) + x);
const auto stride = static_cast<fesa::EntityIndex>(divisions + 1U);
const auto label =
static_cast<std::int64_t>(definition.shell_elements.size() + 1U);
definition.shell_elements.push_back(
{{"Plate", label, std::to_string(label)},
fesa::ShellSourceElementType::kS4,
{first, first + 1U, first + stride + 1U, first + stride},
0U,
0U,
{}});
}
}
fesa::DynamicStepDefinition step{};
step.source_step_name_raw = "C08";
step.source_step_name_canonical = "C08";
step.initial_increment = 1.0 / 64.0;
step.time_period = kEndTime;
step.increment_count = 16U;
if (lifecycle_translation) {
std::vector<fesa::EntityIndex> nodes;
for (std::size_t node = 0U; node < definition.nodes.size(); ++node)
nodes.push_back(static_cast<fesa::EntityIndex>(node));
definition.node_sets.push_back({"All", std::nullopt, nodes, {}});
step.loads = {{"All", 1, 0.2, {}}};
step.increment_count = 2U;
step.time_period = 2.0 * step.initial_increment;
}
definition.dynamic_step = step;
auto domain = std::make_unique<fesa::Domain>(
Require(fesa::Domain::Create(std::move(definition))));
auto model = Require(fesa::AnalysisModel::Create(*domain));
auto dofs = Require(fesa::DofManager::Create(model));
auto shells = Require(fesa::hht_internal::BuildRuntimeShells(*domain, dofs));
const double length = Require(fesa::hht_internal::ReferenceLength(*domain));
return {std::move(domain), std::move(dofs), std::move(shells), length};
}
Frame EmptyFrame(const Fixture& fixture) {
Frame frame{};
frame.is_initial = true;
for (auto* vector :
{&frame.generalized_coordinates, &frame.velocity, &frame.acceleration,
&frame.algorithmic_acceleration, &frame.external_force}) {
*vector = fesa::Vector(fixture.dofs.FullDofCount());
}
return frame;
}
Assembly Evaluate(const Fixture& fixture, Frame frame, const double h,
const fesa::HhtParameters& parameters, const bool third) {
const fesa::SerialParallelFor parallel;
fesa::Vector load_scale(frame.external_force.Size());
for (std::size_t i = 0; i < load_scale.Size(); ++i)
load_scale[i] = std::abs(frame.external_force[i]);
return Require(fesa::hht_internal::EvaluateAssembly(
*fixture.domain, fixture.dofs, fixture.shells, std::move(frame),
load_scale, h, parameters, parallel, third));
}
struct VolumeOracle {
fesa::Vector force;
long double kinetic = 0.0L;
long double power = 0.0L;
long double power_scale = 0.0L;
L3 momentum{};
L3 angular_momentum{};
L3 momentum_rate{};
L3 angular_momentum_rate{};
long double momentum_scale = 0.0L;
long double angular_momentum_scale = 0.0L;
long double momentum_rate_scale = 0.0L;
long double angular_momentum_rate_scale = 0.0L;
};
// Independent nodal 5^3 rectangle quadrature and exponential kinematics.
// No production mass, inertia force, momentum, or director derivative is
// reused.
VolumeOracle Integrate(const Fixture& fixture, const Frame& frame) {
constexpr std::array<long double, 5> points{
-0.906179845938663992797626878299L, -0.538469310105683091036314420700L,
0.0L, 0.538469310105683091036314420700L,
0.906179845938663992797626878299L};
constexpr std::array<long double, 5> weights{
0.236926885056189087514264040720L, 0.478628670499366468041291514836L,
0.568888888888888888888888888889L, 0.478628670499366468041291514836L,
0.236926885056189087514264040720L};
constexpr std::array<std::array<int, 2>, 4> signs{
{{-1, -1}, {1, -1}, {1, 1}, {-1, 1}}};
VolumeOracle oracle{fesa::Vector(fixture.dofs.FullDofCount())};
std::vector<long double> force(fixture.dofs.FullDofCount(), 0.0L);
for (std::size_t element = 0;
element < fixture.domain->ShellElements().Size(); ++element) {
const auto& shell = fixture.domain->ShellElements()[element];
std::array<L3, 4> x{};
std::array<L3, 4> v{};
std::array<L3, 4> a{};
std::array<L3, 4> director{};
std::array<L3, 4> director_rate{};
std::array<L3, 4> director_acceleration{};
std::array<std::array<L3, 3>, 4> derivative{};
for (std::size_t node = 0; node < 4; ++node) {
const auto global = shell.node_indices[node];
const auto offset = 6U * global;
const L3 phi = Triple(frame.generalized_coordinates, offset + 3U);
const L3 phidot = Triple(frame.velocity, offset + 3U);
const L3 phiddot = Triple(frame.acceleration, offset + 3U);
x[node] = Add(Coordinates(fixture.domain->Nodes()[global]),
Triple(frame.generalized_coordinates, offset));
v[node] = Triple(frame.velocity, offset);
a[node] = Triple(frame.acceleration, offset);
director[node] = ExponentialDerivative(phi, {0.0L, 0.0L, 1.0L});
director_rate[node] =
ExponentialDerivative(phi, {0.0L, 0.0L, 1.0L}, {phidot});
director_acceleration[node] =
Add(ExponentialDerivative(phi, {0.0L, 0.0L, 1.0L}, {phidot, phidot}),
ExponentialDerivative(phi, {0.0L, 0.0L, 1.0L}, {phiddot}));
for (std::size_t c = 0; c < 3; ++c) {
L3 axis{};
axis[c] = 1.0L;
derivative[node][c] =
ExponentialDerivative(phi, {0.0L, 0.0L, 1.0L}, {axis});
}
}
const L3 edge_x =
Add(Coordinates(fixture.domain->Nodes()[shell.node_indices[1]]),
Scale(Coordinates(fixture.domain->Nodes()[shell.node_indices[0]]),
-1.0L));
const L3 edge_y =
Add(Coordinates(fixture.domain->Nodes()[shell.node_indices[3]]),
Scale(Coordinates(fixture.domain->Nodes()[shell.node_indices[0]]),
-1.0L));
const long double jacobian = Norm(Cross(edge_x, edge_y)) * 0.025L;
for (std::size_t i = 0; i < 5; ++i) {
for (std::size_t j = 0; j < 5; ++j) {
std::array<long double, 4> shape{};
for (std::size_t node = 0; node < 4; ++node)
shape[node] = 0.25L * (1.0L + signs[node][0] * points[i]) *
(1.0L + signs[node][1] * points[j]);
for (std::size_t k = 0; k < 5; ++k) {
const long double z = 0.1L * points[k];
const long double weight =
jacobian * weights[i] * weights[j] * weights[k];
for (std::size_t node = 0; node < 4; ++node) {
const long double nodal_weight = weight * shape[node];
const L3 position = Add(x[node], Scale(director[node], z));
const L3 velocity = Add(v[node], Scale(director_rate[node], z));
const L3 acceleration =
Add(a[node], Scale(director_acceleration[node], z));
oracle.kinetic += 0.5L * nodal_weight * Dot(velocity, velocity);
oracle.power += nodal_weight * Dot(velocity, acceleration);
oracle.power_scale +=
nodal_weight * Norm(velocity) * Norm(acceleration);
oracle.momentum =
Add(oracle.momentum, Scale(velocity, nodal_weight));
oracle.angular_momentum =
Add(oracle.angular_momentum,
Scale(Cross(position, velocity), nodal_weight));
oracle.momentum_rate =
Add(oracle.momentum_rate, Scale(acceleration, nodal_weight));
oracle.angular_momentum_rate =
Add(oracle.angular_momentum_rate,
Scale(Cross(position, acceleration), nodal_weight));
oracle.momentum_scale += nodal_weight * Norm(velocity);
oracle.angular_momentum_scale +=
nodal_weight * Norm(Cross(position, velocity));
oracle.momentum_rate_scale += nodal_weight * Norm(acceleration);
oracle.angular_momentum_rate_scale +=
nodal_weight * Norm(Cross(position, acceleration));
const auto offset = 6U * shell.node_indices[node];
for (std::size_t c = 0; c < 3; ++c) {
force[offset + c] += nodal_weight * acceleration[c];
force[offset + 3U + c] +=
nodal_weight * z * Dot(derivative[node][c], acceleration);
}
}
}
}
}
}
for (std::size_t i = 0; i < force.size(); ++i)
oracle.force[i] = static_cast<double>(force[i]);
return oracle;
}
std::array<L3, 5> OrbitChart(const double time) {
const long double t = time;
return {{{0.4L * std::sin(t), 0.6L * std::sin(0.7L * t),
0.2L * std::sin(1.3L * t)},
{0.4L * std::cos(t), 0.42L * std::cos(0.7L * t),
0.26L * std::cos(1.3L * t)},
{-0.4L * std::sin(t), -0.294L * std::sin(0.7L * t),
-0.338L * std::sin(1.3L * t)},
{-0.4L * std::cos(t), -0.2058L * std::cos(0.7L * t),
-0.4394L * std::cos(1.3L * t)},
{0.4L * std::sin(t), 0.14406L * std::sin(0.7L * t),
0.57122L * std::sin(1.3L * t)}}};
}
Frame RigidOrbit(const Fixture& fixture, const double time) {
Frame frame = EmptyFrame(fixture);
frame.time = time;
frame.is_initial = time == 0.0;
const auto chart = OrbitChart(time);
const auto& phi = chart[0];
const auto& velocity = chart[1];
const auto& acceleration = chart[2];
for (std::size_t node = 0; node < fixture.domain->Nodes().size(); ++node) {
const auto offset = 6U * node;
const L3 initial = Coordinates(fixture.domain->Nodes()[node]);
Store(Add(ExponentialDerivative(phi, initial), Scale(initial, -1.0L)),
&frame.generalized_coordinates, offset);
Store(phi, &frame.generalized_coordinates, offset + 3U);
Store(ExponentialDerivative(phi, initial, {velocity}), &frame.velocity,
offset);
Store(velocity, &frame.velocity, offset + 3U);
Store(Add(ExponentialDerivative(phi, initial, {velocity, velocity}),
ExponentialDerivative(phi, initial, {acceleration})),
&frame.acceleration, offset);
Store(acceleration, &frame.acceleration, offset + 3U);
}
frame.algorithmic_acceleration = frame.acceleration;
return frame;
}
std::array<L3, 3> OrbitPhysicalRates(const std::array<L3, 5>& chart,
const L3& initial) {
const auto& q = chart[0];
const auto& v = chart[1];
const auto& a = chart[2];
const auto& j = chart[3];
const auto& s = chart[4];
const auto d2 = Add(ExponentialDerivative(q, initial, {v, v}),
ExponentialDerivative(q, initial, {a}));
const auto d3 =
Add(Add(ExponentialDerivative(q, initial, {v, v, v}),
Scale(ExponentialDerivative(q, initial, {v, a}), 3.0L)),
ExponentialDerivative(q, initial, {j}));
const auto d4 =
Add(Add(ExponentialDerivative(q, initial, {v, v, v, v}),
Scale(ExponentialDerivative(q, initial, {v, v, a}), 6.0L)),
Add(Scale(ExponentialDerivative(q, initial, {a, a}), 3.0L),
Add(Scale(ExponentialDerivative(q, initial, {v, j}), 4.0L),
ExponentialDerivative(q, initial, {s}))));
return {d2, d3, d4};
}
// For the single centered rectangle, the nodal kinetic measure gives
// m_i=0.2 and j_i=m_i*t^2/12. Differentiate the nodal force moments.
std::array<fesa::Vector, 2> RigidForceRates(const Fixture& fixture,
const double time) {
std::array<fesa::Vector, 2> rates{fesa::Vector(24U), fesa::Vector(24U)};
const auto chart = OrbitChart(time);
const auto director = OrbitPhysicalRates(chart, {0.0L, 0.0L, 1.0L});
for (std::size_t node = 0; node < 4U; ++node) {
const auto x =
OrbitPhysicalRates(chart, Coordinates(fixture.domain->Nodes()[node]));
Store(Scale(x[1], 0.2L), &rates[0], 6U * node);
Store(Scale(x[2], 0.2L), &rates[1], 6U * node);
for (std::size_t c = 0; c < 3U; ++c) {
L3 axis{};
axis[c] = 1.0L;
const auto g =
ExponentialDerivative(chart[0], {0.0L, 0.0L, 1.0L}, {axis});
const auto dg =
ExponentialDerivative(chart[0], {0.0L, 0.0L, 1.0L}, {axis, chart[1]});
const auto ddg = Add(ExponentialDerivative(chart[0], {0.0L, 0.0L, 1.0L},
{axis, chart[1], chart[1]}),
ExponentialDerivative(chart[0], {0.0L, 0.0L, 1.0L},
{axis, chart[2]}));
rates[0][6U * node + 3U + c] = static_cast<double>(
(0.008L / 12.0L) * (Dot(dg, director[0]) + Dot(g, director[1])));
rates[1][6U * node + 3U + c] =
static_cast<double>((0.008L / 12.0L) * (Dot(ddg, director[0]) +
2.0L * Dot(dg, director[1]) +
Dot(g, director[2])));
}
}
return rates;
}
double SparseNorm(const fesa::SparseMatrix& matrix) {
double norm = 0.0;
for (const double value : matrix.Values()) norm = std::hypot(norm, value);
return norm;
}
// The manufactured F(t) is prescribed in generalized coordinates. Its null
// work vanishes on the exact orbit, but not at a Newton trial. Include every
// explicit-time term of psi=n(q)^T(f_int(q)-F(t)); the public dead-load helper
// deliberately does not implement this verification-only load convention.
void ManufacturedHidden(const Assembly& assembly,
const std::array<fesa::Vector, 2>& force_rates,
Frame* frame) {
const auto nodes = assembly.rotations.size();
for (auto* vector :
{&frame->hidden_constraint, &frame->hidden_constraint_scale,
&frame->hidden_velocity_defect, &frame->hidden_velocity_defect_scale,
&frame->hidden_acceleration_defect,
&frame->hidden_acceleration_defect_scale})
*vector = fesa::Vector(nodes);
for (std::size_t node = 0; node < nodes; ++node) {
const auto& rotation = assembly.rotations[node];
double psi = 0.0;
double psi_scale = 0.0;
double first = 0.0;
double first_scale = 0.0;
double second = assembly.hidden_second[node];
double second_scale = assembly.hidden_second_scale[node];
auto add = [](const double term, double* value, double* scale) {
*value += term;
*scale += std::abs(term);
};
for (std::size_t c = 0; c < 3U; ++c) {
const auto row = 6U * node + 3U + c;
const double n = rotation.drill_null[c];
const double g = frame->internal_force[row] - frame->external_force[row];
psi += n * g;
psi_scale += std::abs(n) * frame->residual_scale[row];
for (std::size_t d = 0; d < 3U; ++d) {
const auto column = 6U * node + 3U + d;
const double dn = rotation.null_gradient(c, d);
add(dn * g * frame->velocity[column], &first, &first_scale);
add(dn * g * frame->acceleration[column], &second, &second_scale);
add(-2.0 * dn * frame->velocity[column] * force_rates[0][row], &second,
&second_scale);
}
const auto& stiffness = assembly.internal_tangent;
for (std::size_t index = stiffness.RowOffsets()[row];
index < stiffness.RowOffsets()[row + 1U]; ++index) {
const auto column = stiffness.ColumnIndices()[index];
const double gradient = n * stiffness.Values()[index];
add(gradient * frame->velocity[column], &first, &first_scale);
add(gradient * frame->acceleration[column], &second, &second_scale);
}
add(-n * force_rates[0][row], &first, &first_scale);
add(-n * force_rates[1][row], &second, &second_scale);
}
frame->hidden_constraint[node] = psi;
frame->hidden_constraint_scale[node] = psi_scale;
frame->hidden_velocity_defect[node] = first;
frame->hidden_velocity_defect_scale[node] = first_scale;
frame->hidden_acceleration_defect[node] = second;
frame->hidden_acceleration_defect_scale[node] = second_scale;
}
}
void CompleteManufacturedConstraints(
const Fixture& fixture, const Assembly& assembly,
const std::vector<std::size_t>& initial_null_nodes,
fesa::MklPardisoSolver* positive, fesa::MklPardisoTangentSolver* tangent,
Frame* frame) {
// This read-only probe exercises current positive mass, null inventory,
// psi and psi_q regularity. psi_q includes n'*(f_int-F), including moments.
// Its autonomous hidden-rate outputs are discarded. Explicit time terms
// and initial rate consistency are evaluated separately below.
auto probe = assembly.frame;
probe.is_initial = false;
Require(fesa::hht_internal::CompleteConstraintEvidence(
*fixture.domain, fixture.dofs, assembly, fixture.length,
initial_null_nodes, *positive, *tangent, &probe));
ManufacturedHidden(assembly, RigidForceRates(fixture, frame->time), frame);
if (frame->is_initial) {
for (std::size_t node = 0; node < assembly.rotations.size(); ++node) {
for (const auto quantity :
{std::make_pair(&frame->hidden_velocity_defect,
&frame->hidden_velocity_defect_scale),
std::make_pair(&frame->hidden_acceleration_defect,
&frame->hidden_acceleration_defect_scale)}) {
const double value = std::abs((*quantity.first)[node]);
const double scale = (*quantity.second)[node];
if (scale == 0.0)
EXPECT_DOUBLE_EQ(value, 0.0);
else
EXPECT_LE(value / scale, 1.0e-10);
}
}
}
}
TEST(HhtShellPhysics, NodalRigidForceRatesMatchIndependentTimeDifferences) {
const auto fixture = MakeFixture(1U);
constexpr double time = 0.37;
const auto rates = RigidForceRates(fixture, time);
for (const double delta : {0.02, 0.01, 0.005}) {
SCOPED_TRACE(delta);
std::array<fesa::Vector, 5> forces{fesa::Vector(24U), fesa::Vector(24U),
fesa::Vector(24U), fesa::Vector(24U),
fesa::Vector(24U)};
for (std::size_t sample = 0; sample < forces.size(); ++sample) {
const double sample_time =
time + (static_cast<double>(sample) - 2.0) * delta;
forces[sample] =
Integrate(fixture, RigidOrbit(fixture, sample_time)).force;
}
for (std::size_t derivative = 0; derivative < 2U; ++derivative) {
for (const bool rotary : {false, true}) {
SCOPED_TRACE(derivative);
SCOPED_TRACE(rotary);
double error = 0.0;
double scale = 0.0;
for (std::size_t row = 0; row < 24U; ++row) {
if ((row % 6U >= 3U) != rotary) continue;
const double difference =
derivative == 0U ? (forces[0][row] - 8.0 * forces[1][row] +
8.0 * forces[3][row] - forces[4][row]) /
(12.0 * delta)
: (-forces[0][row] + 16.0 * forces[1][row] -
30.0 * forces[2][row] +
16.0 * forces[3][row] - forces[4][row]) /
(12.0 * delta * delta);
error = std::hypot(error, rates[derivative][row] - difference);
scale = std::hypot(scale, difference);
}
ASSERT_GT(scale, 0.0);
EXPECT_LE(error / scale, 1.0e-6);
}
}
}
}
TEST(HhtShellPhysics, ExternalDrillingHessianBalancesGradientAcceleration) {
const auto fixture = MakeFixture(1U);
const fesa::Vector3 phi{0.0, 0.0, 0.0};
const fesa::Vector3 velocity{1.0, 0.0, 1.0};
const fesa::Vector3 acceleration{0.0, -1.0 / 3.0, 0.0};
const fesa::Vector3 force{1.0, 0.0, 0.0};
const auto external = fixture.shells[0U].internal->EvaluateExternalDrillingConstraint(
0U, phi, velocity, force, {}, {});
ASSERT_TRUE(external.HasValue());
double gradient_acceleration = 0.0;
for (std::size_t component = 0U; component < 3U; ++component)
gradient_acceleration +=
external.Value().gradient[component] * acceleration[component];
EXPECT_NEAR(external.Value().directional_second, -1.0 / 6.0, 1.0e-14);
EXPECT_NEAR(gradient_acceleration, 1.0 / 6.0, 1.0e-14);
EXPECT_NEAR(external.Value().directional_second + gradient_acceleration,
0.0, 1.0e-14);
constexpr double step = 1.0e-4;
const auto plus = fixture.shells[0U].internal->EvaluateExternalDrillingConstraint(
0U, {step, 0.0, step}, {}, force, {}, {});
const auto middle = fixture.shells[0U].internal->EvaluateExternalDrillingConstraint(
0U, phi, {}, force, {}, {});
const auto minus = fixture.shells[0U].internal->EvaluateExternalDrillingConstraint(
0U, {-step, 0.0, -step}, {}, force, {}, {});
ASSERT_TRUE(plus.HasValue());
ASSERT_TRUE(middle.HasValue());
ASSERT_TRUE(minus.HasValue());
const double finite_difference =
(plus.Value().value - 2.0 * middle.Value().value + minus.Value().value) /
(step * step);
EXPECT_NEAR(finite_difference, external.Value().directional_second, 1.0e-8);
}
TEST(HhtShellPhysics, ManufacturedHiddenCountsExternalHessianOnce) {
const auto fixture = MakeFixture(1U);
auto frame = EmptyFrame(fixture);
frame.velocity[3U] = frame.velocity[5U] = 1.0;
frame.acceleration[4U] = -1.0 / 3.0;
frame.external_force[3U] = 1.0;
const auto parameters = Require(fesa::MakeHhtParameters(-0.05));
const auto assembly = Evaluate(fixture, frame, 1.0 / 64.0, parameters, true);
auto balance = assembly.frame;
std::array<fesa::Vector, 2U> zero_rates{
fesa::Vector(fixture.dofs.FullDofCount()),
fesa::Vector(fixture.dofs.FullDofCount())};
ManufacturedHidden(assembly, zero_rates, &balance);
constexpr std::size_t node = 0U;
double expected = assembly.hidden_second[node];
const auto& rotation = assembly.rotations[node];
for (std::size_t component = 0U; component < 3U; ++component) {
const auto row = 6U * node + 3U + component;
const double force =
assembly.frame.internal_force[row] - assembly.frame.external_force[row];
for (std::size_t direction = 0U; direction < 3U; ++direction)
expected += rotation.null_gradient(component, direction) * force *
assembly.frame.acceleration[6U * node + 3U + direction];
for (std::size_t index = assembly.internal_tangent.RowOffsets()[row];
index < assembly.internal_tangent.RowOffsets()[row + 1U]; ++index)
expected += rotation.drill_null[component] *
assembly.internal_tangent.Values()[index] *
assembly.frame.acceleration[
assembly.internal_tangent.ColumnIndices()[index]];
}
const double scale = assembly.hidden_second_scale[node] + std::abs(expected);
ASSERT_GT(scale, 0.0);
EXPECT_LE(std::abs(balance.hidden_acceleration_defect[node] - expected) /
scale,
1.0e-12);
}
TEST(HhtShellPhysics,
C08aRigidOrbitMatchesIndependentVolumeForcePowerAndMomentum) {
const auto fixture = MakeFixture(1U);
const auto parameters = Require(fesa::MakeHhtParameters(-0.05));
const auto initial =
Evaluate(fixture, EmptyFrame(fixture), 1.0 / 16.0, parameters, false);
const auto scaled_stiffness = Require(fesa::hht_internal::ScaledFree(
initial.internal_tangent, fixture.dofs, fixture.length));
double maximum_rotation = 0.0;
for (std::size_t sample = 0; sample <= 16; ++sample) {
const double time = static_cast<double>(sample) / 16.0;
SCOPED_TRACE(time);
auto exact = RigidOrbit(fixture, time);
const auto oracle = Integrate(fixture, exact);
exact.external_force = oracle.force;
const auto assembly =
Evaluate(fixture, exact, 1.0 / 16.0, parameters, true);
auto difference = assembly.frame.inertia_force;
difference.Axpy(-1.0, oracle.force);
EXPECT_LE(difference.Norm() / oracle.force.Norm(), 1.0e-12);
EXPECT_LE(std::abs(assembly.frame.kinetic_energy -
static_cast<double>(oracle.kinetic)) /
static_cast<double>(oracle.kinetic),
1.0e-12);
EXPECT_LE(std::abs(oracle.force.Dot(exact.velocity) -
static_cast<double>(oracle.power)) /
static_cast<double>(oracle.power_scale),
1.0e-10);
EXPECT_LE(assembly.frame.physical_strain_energy / 800.0, 1.0e-10);
EXPECT_LE(assembly.frame.drilling_energy / 800.0, 1.0e-10);
fesa::Vector scaled_q = exact.generalized_coordinates;
fesa::Vector scaled_force = assembly.frame.internal_force;
for (std::size_t i = 0; i < scaled_q.Size(); ++i) {
if (i % 6U < 3U) {
scaled_q[i] /= fixture.length;
scaled_force[i] *= fixture.length;
}
}
const double force_scale = SparseNorm(scaled_stiffness) * scaled_q.Norm();
if (force_scale == 0.0)
EXPECT_DOUBLE_EQ(scaled_force.Norm(), 0.0);
else
EXPECT_LE(scaled_force.Norm() / force_scale, 1.0e-10);
double momentum_error = 0.0;
double angular_error = 0.0;
for (std::size_t c = 0; c < 3; ++c) {
momentum_error = std::hypot(momentum_error,
assembly.frame.linear_momentum[c] -
static_cast<double>(oracle.momentum[c]));
angular_error = std::hypot(
angular_error, assembly.frame.angular_momentum[c] -
static_cast<double>(oracle.angular_momentum[c]));
}
EXPECT_LE(momentum_error / static_cast<double>(oracle.momentum_scale),
1.0e-12);
EXPECT_LE(
angular_error / static_cast<double>(oracle.angular_momentum_scale),
1.0e-12);
auto balance = assembly.frame;
ManufacturedHidden(assembly, RigidForceRates(fixture, time), &balance);
for (std::size_t node = 0; node < 4U; ++node) {
for (const auto quantity :
{std::make_pair(&balance.hidden_constraint,
&balance.hidden_constraint_scale),
std::make_pair(&balance.hidden_velocity_defect,
&balance.hidden_velocity_defect_scale),
std::make_pair(&balance.hidden_acceleration_defect,
&balance.hidden_acceleration_defect_scale)}) {
const double value = std::abs((*quantity.first)[node]);
const double scale = (*quantity.second)[node];
if (scale == 0.0)
EXPECT_DOUBLE_EQ(value, 0.0);
else
EXPECT_LE(value / scale, 1.0e-10);
}
}
EXPECT_TRUE(fesa::hht_internal::CompleteEquilibrium(
*fixture.domain, fixture.dofs, assembly, &balance)
.IsOk());
const double mass_norm = SparseNorm(assembly.mass);
for (std::size_t node = 0; node < 4U; ++node) {
fesa::Vector null(24U);
for (std::size_t c = 0; c < 3U; ++c)
null[6U * node + 3U + c] = assembly.rotations[node].drill_null[c];
EXPECT_LE(assembly.mass.Multiply(null).Norm() / mass_norm, 1.0e-12);
}
maximum_rotation = std::max(
maximum_rotation,
static_cast<double>(Norm(Triple(exact.generalized_coordinates, 3U))));
}
std::cout << "C08a pointwise independent-volume max_rotation="
<< std::setprecision(17) << maximum_rotation << '\n';
EXPECT_GT(maximum_rotation, 0.5);
}
// At q0=0 the normalized null is e_phi3 and f_int=0, hence psi_q is the
// corresponding stiffness row. Determine all null velocities, rather than
// assigning the unobservable director-axis chart rate arbitrarily.
void InitializeNullVelocity(const Fixture& fixture, const Assembly& initial,
Frame* frame,
fesa::MklPardisoTangentSolver* solver) {
const auto nodes = fixture.domain->Nodes().size();
std::vector<fesa::CooContribution> entries;
fesa::Vector rhs(nodes);
for (std::size_t node = 0; node < nodes; ++node) {
const auto row = 6U * node + 5U;
for (auto offset = initial.internal_tangent.RowOffsets()[row];
offset < initial.internal_tangent.RowOffsets()[row + 1U]; ++offset) {
const auto column = initial.internal_tangent.ColumnIndices()[offset];
const double value = initial.internal_tangent.Values()[offset];
if (column % 6U == 5U)
entries.push_back({node, column / 6U, value, 0U, entries.size()});
else
rhs[node] -= value * frame->velocity[column];
}
}
const auto regularity =
Require(fesa::hht_internal::MakeSparse(nodes, nodes, std::move(entries)));
fesa::Vector null_velocity(nodes);
Require(solver->FactorizeFresh(regularity));
Require(solver->SolveCorrection(rhs, null_velocity));
Require(fesa::hht_internal::CheckLinearSolve(regularity, rhs, null_velocity));
for (std::size_t node = 0; node < nodes; ++node)
frame->velocity[6U * node + 5U] = null_velocity[node];
}
struct GridEvidence {
double h;
double end_time;
std::vector<Frame> history;
std::array<double, 6> solve_noise{};
std::array<double, 6> arithmetic_noise{};
std::array<double, 6> selector_history_arithmetic_noise{};
std::array<double, 2> full_filter_arithmetic_noise{};
double energy_error = 0.0;
double momentum_error = 0.0;
double angular_momentum_error = 0.0;
double maximum_rotation = 0.0;
double algorithmic_acceleration_gap = 0.0;
double hidden_velocity = 0.0;
double hidden_acceleration = 0.0;
struct RecoveredAcceleration {
fesa::Vector value;
double pre_hidden2 = 0.0;
double pre_hidden2_relative = 0.0;
double post_hidden2 = 0.0;
double post_hidden2_relative = 0.0;
double mass_delta_a_relative = 0.0;
};
// These are diagnostic-only: RunPlate never substitutes them into Frame.
std::vector<RecoveredAcceleration> recovered_acceleration;
};
// Fixed-correction-path rounding radii for the carried-A recurrence. They
// bound arithmetic in the stored operation order only; Newton sensitivity is
// covered separately by the last-solve defect below.
struct ArithmeticRadii {
fesa::Vector coordinates;
fesa::Vector velocity;
fesa::Vector acceleration;
fesa::Vector algorithmic_acceleration;
};
double Up(const double value) {
if (!std::isfinite(value) || !(value >= 0.0))
return std::numeric_limits<double>::infinity();
return std::nextafter(value, std::numeric_limits<double>::infinity());
}
double UpSum(const double left, const double right) {
return Up(left + right);
}
double UpProduct(const double left, const double right) {
// Every caller is an upper nonnegative radius/product operation. Preserve
// exact zero support under an infinite inherited radius, but turn NaN or
// nonfinite nonzero products into infinity so max-based diagnostics cannot
// silently accept an incomplete certificate.
if (std::isnan(left) || std::isnan(right) || !(left >= 0.0) || !(right >= 0.0))
return std::numeric_limits<double>::infinity();
if (left == 0.0 || right == 0.0) return 0.0;
if (!std::isfinite(left) || !std::isfinite(right))
return std::numeric_limits<double>::infinity();
const double product = left * right;
return std::isfinite(product) ? Up(product)
: std::numeric_limits<double>::infinity();
}
double RoundoffRadius(const double value) {
const double u = std::ldexp(1.0, -53);
return UpSum(UpProduct(Up(u / (1.0 - u)), std::abs(value)),
std::numeric_limits<double>::denorm_min());
}
double AddRadius(const double result, const double left, const double right) {
return UpSum(UpSum(left, right), RoundoffRadius(result));
}
double ProductRadius(const double left_value, const double left_radius,
const double right_value, const double right_radius) {
const double result = left_value * right_value;
return UpSum(UpSum(UpProduct(std::abs(right_value), left_radius),
UpProduct(std::abs(left_value), right_radius)),
UpSum(UpProduct(left_radius, right_radius), RoundoffRadius(result)));
}
// Replay the actual fixed-branch Neumaier operations of rate reconstruction.
struct RateSumRadius {
double sum = 0.0, correction = 0.0;
double sum_radius = 0.0, correction_radius = 0.0;
void Add(const double term, const double term_radius) {
if (!std::isfinite(term) || !std::isfinite(term_radius) ||
!(term_radius >= 0.0))
throw std::runtime_error{"C08 arithmetic radius input is incomplete."};
const double next = sum + term;
const double next_radius = AddRadius(next, sum_radius, term_radius);
const bool sum_first = std::abs(sum) >= std::abs(term);
const double first = (sum_first ? sum : term) - next;
const double first_radius = AddRadius(first,
sum_first ? sum_radius : term_radius, next_radius);
const double error = first + (sum_first ? term : sum);
const double error_radius = AddRadius(error, first_radius,
sum_first ? term_radius : sum_radius);
correction += error;
correction_radius = AddRadius(correction, correction_radius, error_radius);
sum = next;
sum_radius = next_radius;
}
double Value() const { return sum + correction; }
double Radius() const {
return AddRadius(Value(), sum_radius, correction_radius);
}
};
double QuotientRadius(const double numerator_value, const double numerator_radius,
const double denominator_value,
const double denominator_radius) {
const double lower = std::nextafter(
std::abs(denominator_value) - denominator_radius,
-std::numeric_limits<double>::infinity());
if (!(lower > 0.0))
throw std::runtime_error{"C08 carried-Ar arithmetic denominator radius lost sign."};
const double result = numerator_value / denominator_value;
const double input = UpSum(numerator_radius,
UpProduct(std::abs(result), denominator_radius));
return UpSum(Up(input / lower), UpProduct(RoundoffRadius(result),
UpSum(1.0, Up(denominator_radius / lower))));
}
namespace reduced = fesa::hht_reduced_internal;
fesa::Vector ProjectBasisRadii(
const std::vector<fesa::hht_reduced_internal::FixedBasisColumn>& columns,
const fesa::Vector& full, const fesa::Vector& full_radii,
fesa::Vector* replay_center = nullptr) {
if (replay_center != nullptr) *replay_center = fesa::Vector(columns.size());
fesa::Vector result(columns.size());
for (std::size_t column = 0U; column < columns.size(); ++column) {
const auto& basis = columns[column];
if (basis.support.size() != basis.support_radii.size())
throw std::runtime_error{"C08 basis projection radius inventory is invalid."};
RateSumRadius sum;
for (std::size_t i = 0U; i < basis.support.size(); ++i) {
const auto& entry = basis.support[i];
if (entry.dof >= full.Size() || entry.dof >= full_radii.Size() ||
!std::isfinite(basis.support_radii[i]))
throw std::runtime_error{"C08 basis projection radius support is invalid."};
const double term = entry.value * full[entry.dof];
const double term_radius = ProductRadius(entry.value, basis.support_radii[i],
full[entry.dof], full_radii[entry.dof]);
sum.Add(term, term_radius);
}
if (replay_center != nullptr) (*replay_center)[column] = sum.Value();
result[column] = sum.Radius();
}
return result;
}
void AccumulateLiftBasisRadii(
const std::vector<fesa::hht_reduced_internal::FixedBasisColumn>& columns,
const fesa::Vector& coordinates, const fesa::Vector& coordinate_radii,
std::vector<RateSumRadius>* sums) {
if (coordinates.Size() != columns.size() || coordinate_radii.Size() != columns.size())
throw std::runtime_error{"C08 basis lift radius inventory is invalid."};
for (std::size_t column = 0U; column < columns.size(); ++column) {
const auto& basis = columns[column];
if (basis.support.size() != basis.support_radii.size())
throw std::runtime_error{"C08 basis lift radius support inventory is invalid."};
for (std::size_t i = 0U; i < basis.support.size(); ++i) {
const auto& entry = basis.support[i];
if (entry.dof >= sums->size() || !std::isfinite(basis.support_radii[i]))
throw std::runtime_error{"C08 basis lift radius support is invalid."};
const double term = entry.value * coordinates[column];
const double term_radius = ProductRadius(entry.value, basis.support_radii[i],
coordinates[column], coordinate_radii[column]);
(*sums)[entry.dof].Add(term, term_radius);
}
}
}
struct BasisLiftReplay {
fesa::Vector center;
fesa::Vector radius;
};
BasisLiftReplay ReplayLiftBasisRadii(
const std::vector<fesa::hht_reduced_internal::FixedBasisColumn>& columns,
const fesa::Vector& coordinates, const fesa::Vector& coordinate_radii,
const std::size_t full_count) {
std::vector<RateSumRadius> sums(full_count);
AccumulateLiftBasisRadii(columns, coordinates, coordinate_radii, &sums);
BasisLiftReplay result{fesa::Vector(full_count), fesa::Vector(full_count)};
for (std::size_t full = 0U; full < full_count; ++full) {
result.center[full] = sums[full].Value();
result.radius[full] = sums[full].Radius();
}
return result;
}
BasisLiftReplay ReplayLiftBasisRadii(
const std::vector<fesa::hht_reduced_internal::FixedBasisColumn>& p_columns,
const fesa::Vector& p, const fesa::Vector& p_radii,
const std::vector<fesa::hht_reduced_internal::FixedBasisColumn>& z_columns,
const fesa::Vector& z, const fesa::Vector& z_radii,
const std::size_t full_count) {
std::vector<RateSumRadius> sums(full_count);
// Match reduced::Lift: every p support precedes every z support.
AccumulateLiftBasisRadii(p_columns, p, p_radii, &sums);
AccumulateLiftBasisRadii(z_columns, z, z_radii, &sums);
BasisLiftReplay result{fesa::Vector(full_count), fesa::Vector(full_count)};
for (std::size_t full = 0U; full < full_count; ++full) {
result.center[full] = sums[full].Value();
result.radius[full] = sums[full].Radius();
}
return result;
}
fesa::Vector LiftBasisRadii(
const std::vector<fesa::hht_reduced_internal::FixedBasisColumn>& columns,
const fesa::Vector& coordinates, const fesa::Vector& coordinate_radii,
const std::size_t full_count) {
return ReplayLiftBasisRadii(columns, coordinates, coordinate_radii, full_count).radius;
}
TEST(HhtShellPhysics, BasisCoefficientRadiusSurvivesSparseProjectionAndLift) {
// A zero stored coefficient still has construction uncertainty. Dropping
// its support radius would incorrectly erase the free angular full-row
// contribution when a nonzero private plane coordinate is lifted.
const fesa::hht_reduced_internal::FixedBasisColumn plane{
0U, 1U, true,
{{3U, 0.0}, {4U, 0.6}, {5U, 0.8}},
{0x1p-52, 0x1p-53, 0x1p-53}};
const std::vector<fesa::hht_reduced_internal::FixedBasisColumn> columns{plane};
fesa::Vector coordinate(1U);
coordinate[0U] = 4.0;
const fesa::Vector zero_coordinate_radius(1U);
const auto lifted = LiftBasisRadii(columns, coordinate,
zero_coordinate_radius, 6U);
EXPECT_GT(lifted[3U], 0.0);
EXPECT_GE(lifted[4U], 4.0 * 0x1p-53);
EXPECT_GE(lifted[5U], 4.0 * 0x1p-53);
fesa::Vector full(6U);
full[4U] = 2.0;
const fesa::Vector zero_full_radius(6U);
const auto projected = ProjectBasisRadii(columns, full, zero_full_radius);
EXPECT_GT(projected[0U], 2.0 * 0x1p-53);
}
TEST(HhtShellPhysics, BasisRadiusReplayMatchesCompensatedProjectionAndCombinedLift) {
const double large = std::ldexp(1.0, 53);
fesa::Vector full(3U);
full[0U] = large;
full[1U] = 1.0;
full[2U] = -large;
const fesa::Vector zero_full_radius(3U);
const reduced::FixedBasisColumn projection{
0U, 0U, false, {{0U, 1.0}, {1U, 1.0}, {2U, 1.0}}, {0.0, 0.0, 0.0}};
fesa::Vector projection_center(0U);
const auto projection_radius = ProjectBasisRadii({projection}, full,
zero_full_radius,
&projection_center);
reduced::FixedChart projection_chart{};
projection_chart.full_dof_count = 3U;
projection_chart.p_dofs = {0U};
projection_chart.p_columns = {projection};
const auto projected = Require(reduced::ProjectPhysical(projection_chart, full));
EXPECT_DOUBLE_EQ(projected[0U], 1.0);
EXPECT_DOUBLE_EQ(projection_center[0U], projected[0U]);
EXPECT_GT(projection_radius[0U], 0.0);
reduced::FixedChart lift_chart{};
lift_chart.full_dof_count = 1U;
lift_chart.p_dofs = {0U, 1U};
lift_chart.z_dofs = {2U};
lift_chart.p_columns = {
{0U, 0U, false, {{0U, 1.0}}, {0.0}},
{0U, 0U, false, {{0U, 1.0}}, {0.0}}};
lift_chart.z_columns = {{0U, 1U, false, {{0U, 1.0}}, {0.0}}};
fesa::Vector p(2U);
p[0U] = large;
p[1U] = 1.0;
const fesa::Vector z{1U, -large};
const auto replay = ReplayLiftBasisRadii(lift_chart.p_columns, p, fesa::Vector(2U),
lift_chart.z_columns, z, fesa::Vector(1U), 1U);
const auto lifted = Require(reduced::Lift(lift_chart, p, z));
EXPECT_DOUBLE_EQ(lifted[0U], 1.0);
EXPECT_DOUBLE_EQ(replay.center[0U], lifted[0U]);
double plain = 0.0;
for (const double value : {large, 1.0, -large}) plain += value;
EXPECT_DOUBLE_EQ(plain, 0.0);
const double target = 1.25;
const double gap = target - replay.center[0U];
const double target_radius = UpSum(replay.radius[0U],
UpSum(std::abs(gap), RoundoffRadius(gap)));
EXPECT_GE(target_radius, 0.25);
fesa::Vector small_p(2U);
small_p[0U] = 1.0;
const auto small_replay = ReplayLiftBasisRadii(
lift_chart.p_columns, small_p, fesa::Vector(2U), lift_chart.z_columns,
fesa::Vector(1U), fesa::Vector(1U), 1U);
const auto small_lifted = Require(reduced::Lift(
lift_chart, small_p, fesa::Vector(1U)));
EXPECT_DOUBLE_EQ(small_replay.center[0U], small_lifted[0U]);
EXPECT_DOUBLE_EQ(small_replay.center[0U], 1.0);
ASSERT_LT(small_replay.radius[0U], 0.25);
const double small_gap = 1.25 - small_replay.center[0U];
const double small_target_radius = UpSum(
small_replay.radius[0U],
UpSum(std::abs(small_gap), RoundoffRadius(small_gap)));
EXPECT_GE(small_target_radius, 0.25);
}
TEST(HhtShellPhysics, BasisLiftedAlgorithmicCorrectionUsesHhtFilterFactor) {
reduced::FixedChart chart{};
chart.full_dof_count = 2U;
chart.p_dofs = {0U};
chart.p_columns = {{0U, 1U, true, {{0U, 0.6}, {1U, 0.8}}, {0.0, 0.0}}};
fesa::Vector delta_a(2U);
delta_a[0U] = 1.2;
delta_a[1U] = 1.6;
const auto parameters = Require(fesa::MakeHhtParameters(-0.05));
const double delta_ar = (1.0 + parameters.alpha) * 2.0;
fesa::Vector delta_A = delta_a;
delta_A.Scale(1.0 + parameters.alpha);
const auto projected = Require(reduced::ProjectPhysical(chart, delta_A));
EXPECT_NEAR(projected[0U], delta_ar, 1.0e-15);
const auto omitted = Require(reduced::ProjectPhysical(chart, delta_a));
const double omission_scale = std::abs(omitted[0U]) + std::abs(delta_ar);
ASSERT_GT(omission_scale, 0.0);
EXPECT_GT(std::abs(omitted[0U] - delta_ar) / omission_scale, 1.0e-12);
}
TEST(HhtShellPhysics, CarriedArRadiiIncludeAbsorbedUpdatesAndInputHistory) {
ASSERT_EQ(std::fegetround(), FE_TONEAREST);
volatile double subnormal = std::numeric_limits<double>::denorm_min();
volatile double doubled = subnormal + subnormal;
ASSERT_DOUBLE_EQ(doubled, 2.0 * std::numeric_limits<double>::denorm_min());
const double tiny = std::ldexp(1.0, -54);
const double rounded = 1.0 + tiny;
ASSERT_DOUBLE_EQ(rounded, 1.0);
const double first = AddRadius(rounded, 0.0, 0.0);
EXPECT_GE(first, tiny);
const double second = AddRadius(rounded + tiny, first, 0.0);
EXPECT_GT(second, first);
EXPECT_GE(second, 2.0 * tiny);
EXPECT_GE(ProductRadius(2.0, tiny, 4.0, tiny), 6.0 * tiny);
EXPECT_GE(QuotientRadius(1.0, tiny, 2.0, 0.0), 0.5 * tiny);
EXPECT_THROW(QuotientRadius(1.0, 0.0, 1.0, 1.0), std::runtime_error);
EXPECT_TRUE(std::isinf(AddRadius(std::numeric_limits<double>::max(),
std::numeric_limits<double>::max(), 0.0)));
}
TEST(HhtShellPhysics, PrimaryCoordinateRetainsCorrectionAbsorbedByCarriedAr) {
// The unchanged LargeDIRECT fixture has a0=-800000, h=.25 and this pp.
const double pp = -11218.75;
const double factor = 0.275625 * 0.25 * 0.25;
const double ar = -pp / factor;
const double initial_p = pp + factor * ar;
const double delta_p = 1.0e-12;
const double next_ar = ar + delta_p / factor;
EXPECT_DOUBLE_EQ(pp + factor * next_ar, initial_p);
const double next_p = initial_p + delta_p;
EXPECT_GT(next_p, initial_p);
EXPECT_DOUBLE_EQ(next_p - initial_p, delta_p);
const double scale = std::abs(pp) + std::abs(factor * next_ar);
EXPECT_LE(std::abs(next_p - (pp + factor * next_ar)) / scale, 1.0e-12);
const double inherited_radius = 2.0e-10;
const double first_radius = AddRadius(next_p, inherited_radius, 0.0);
EXPECT_GT(first_radius, inherited_radius);
EXPECT_GT(AddRadius(next_p + delta_p, first_radius, 0.0), first_radius);
}
// Diagnostic error-free transform of one finite binary64 addition: exact
// input sum minus the stored sum. It does not modify the integrator state.
double AdditionLowPart(const double left, const double right, const double sum) {
const double right_virtual = sum - left;
return (left - (sum - right_virtual)) + (right - right_virtual);
}
// TwoSum preserves the exact high+delta sum as a pair. Only adding its low
// part to the previous low part rounds the represented pair total. The owned
// history radius enters once; the unused final low part encloses high output.
void AdvanceCoordinate(const double delta, double* high, double* low,
double* pair_radius) {
const double sum = *high + delta;
const double error = AdditionLowPart(*high, delta, sum);
const double low_sum = *low + error;
*high = sum + low_sum;
*low = AdditionLowPart(sum, low_sum, *high);
*pair_radius = UpSum(*pair_radius, RoundoffRadius(low_sum));
}
TEST(HhtShellPhysics, CompensatedCoordinateRetainsAbsorbedNewtonCorrections) {
const double delta = std::ldexp(1.0, -54);
double high = 1.0, low = 0.0, radius = 2.0e-10;
const double inherited_radius = radius;
AdvanceCoordinate(delta, &high, &low, &radius);
EXPECT_DOUBLE_EQ(high, 1.0);
EXPECT_DOUBLE_EQ(low, delta);
for (unsigned i = 1U; i < 4U; ++i)
AdvanceCoordinate(delta, &high, &low, &radius);
EXPECT_DOUBLE_EQ(high, 1.0 + std::ldexp(1.0, -52));
EXPECT_DOUBLE_EQ(low, 0.0);
EXPECT_GT(radius, inherited_radius);
EXPECT_GE(UpSum(radius, std::abs(low)), inherited_radius);
high = 1.0;
const double first = 1.0 + delta;
low = AdditionLowPart(1.0, delta, first) + AdditionLowPart(first, delta, first + delta);
radius = 0.0;
AdvanceCoordinate(delta, &high, &low, &radius);
EXPECT_DOUBLE_EQ(high, 1.0 + std::ldexp(1.0, -52));
EXPECT_DOUBLE_EQ(low, -delta);
high = std::numeric_limits<double>::denorm_min();
low = 0.0;
radius = 0.0;
AdvanceCoordinate(high, &high, &low, &radius);
EXPECT_DOUBLE_EQ(high, 2.0 * std::numeric_limits<double>::denorm_min());
EXPECT_DOUBLE_EQ(low, 0.0);
high = std::numeric_limits<double>::max();
AdvanceCoordinate(high, &high, &low, &radius);
EXPECT_FALSE(std::isfinite(high));
EXPECT_FALSE(std::isfinite(radius));
}
double HhtCoordinateFactorRadius(const fesa::HhtParameters& p, const double h) {
const double first = p.beta * h;
return ProductRadius(first, RoundoffRadius(first), h, 0.0);
}
double RadiusNorm(const fesa::Vector& radii, const std::size_t offset) {
if (offset + 3U > radii.Size())
throw std::runtime_error{"C08 radius channel is outside the full vector."};
return std::hypot(radii[offset], radii[offset + 1U], radii[offset + 2U]);
}
struct GraphBasisProducts {
fesa::Matrix graph;
fesa::Matrix physical;
fesa::Matrix graph_radius;
fesa::Matrix physical_radius;
};
struct GraphInverseCertificate {
fesa::Matrix inverse_action;
fesa::Matrix inverse_bound;
fesa::Matrix residual_operator_bound;
bool complete = false;
};
GraphBasisProducts ContractGraphBasis(
const reduced::ConstraintOperators& operators,
const reduced::FixedChart& chart) {
const auto contract = [&](const std::vector<reduced::FixedBasisColumn>& columns) {
fesa::Matrix value(operators.rows.size(), columns.size());
fesa::Matrix radius(operators.rows.size(), columns.size());
for (std::size_t row = 0U; row < operators.rows.size(); ++row) {
for (std::size_t column = 0U; column < columns.size(); ++column) {
const auto& basis = columns[column];
if (basis.support.size() != basis.support_radii.size())
throw std::runtime_error{"C08 graph basis radius inventory is invalid."};
RateSumRadius sum;
for (const auto& gradient : operators.rows[row].gradient) {
for (std::size_t support = 0U; support < basis.support.size(); ++support) {
const auto& entry = basis.support[support];
if (gradient.dof != entry.dof) continue;
sum.Add(gradient.value * entry.value,
ProductRadius(gradient.value, 0.0, entry.value,
basis.support_radii[support]));
}
}
value(row, column) = sum.Value();
radius(row, column) = sum.Radius();
}
}
return std::pair{std::move(value), std::move(radius)};
};
auto [graph, graph_radius] = contract(chart.z_columns);
auto [physical, physical_radius] = contract(chart.p_columns);
return {std::move(graph), std::move(physical), std::move(graph_radius),
std::move(physical_radius)};
}
fesa::SparseMatrix SparseFromDense(const fesa::Matrix& matrix) {
std::vector<fesa::CooContribution> entries;
for (std::size_t row = 0U; row < matrix.Rows(); ++row)
for (std::size_t column = 0U; column < matrix.Columns(); ++column)
if (matrix(row, column) != 0.0)
entries.push_back({row, column, matrix(row, column), 0U, entries.size()});
return Require(fesa::hht_internal::MakeSparse(matrix.Rows(), matrix.Columns(),
std::move(entries)));
}
double UpperRowSum(const fesa::Matrix& matrix, const std::size_t row) {
double sum = 0.0;
for (std::size_t column = 0U; column < matrix.Columns(); ++column)
sum = UpSum(sum, std::abs(matrix(row, column)));
return sum;
}
bool FiniteMatrix(const fesa::Matrix& matrix) {
for (std::size_t row = 0U; row < matrix.Rows(); ++row)
for (std::size_t column = 0U; column < matrix.Columns(); ++column)
if (!std::isfinite(matrix(row, column))) return false;
return true;
}
bool FiniteNonnegativeMatrix(const fesa::Matrix& matrix) {
for (std::size_t row = 0U; row < matrix.Rows(); ++row)
for (std::size_t column = 0U; column < matrix.Columns(); ++column)
if (!std::isfinite(matrix(row, column)) || !(matrix(row, column) >= 0.0))
return false;
return true;
}
bool FiniteVector(const fesa::Vector& vector) {
for (std::size_t i = 0U; i < vector.Size(); ++i)
if (!std::isfinite(vector[i])) return false;
return true;
}
bool FiniteNonnegativeVector(const fesa::Vector& vector) {
for (std::size_t i = 0U; i < vector.Size(); ++i)
if (!std::isfinite(vector[i]) || !(vector[i] >= 0.0)) return false;
return true;
}
bool HasInfiniteRadius(const fesa::Vector& vector) {
bool infinite = false;
for (std::size_t i = 0U; i < vector.Size(); ++i) {
if (std::isnan(vector[i]) || !(vector[i] >= 0.0))
throw std::runtime_error{"C08 graph arithmetic radius is invalid."};
infinite = infinite || std::isinf(vector[i]);
}
return infinite;
}
void FillInfinite(fesa::Vector* vector) {
for (std::size_t i = 0U; i < vector->Size(); ++i)
(*vector)[i] = std::numeric_limits<double>::infinity();
}
GraphInverseCertificate CertifyGraphInverse(const GraphBasisProducts& products,
fesa::TangentSolver* solver) {
const auto size = products.graph.Rows();
if (products.graph.Columns() != size || products.graph_radius.Rows() != size ||
products.graph_radius.Columns() != size)
throw std::runtime_error{"C08 graph certificate matrix inventory is invalid."};
if (!FiniteMatrix(products.graph) ||
!FiniteNonnegativeMatrix(products.graph_radius))
return {fesa::Matrix(size, size), fesa::Matrix(size, size),
fesa::Matrix(size, size), false};
if (size == 0U)
return {fesa::Matrix(0U, 0U), fesa::Matrix(0U, 0U), fesa::Matrix(0U, 0U), true};
const auto sparse = SparseFromDense(products.graph);
Require(solver->FactorizeFresh(sparse));
fesa::Matrix inverse(size, size);
for (std::size_t column = 0U; column < size; ++column) {
fesa::Vector unit(size), action(size);
unit[column] = 1.0;
Require(solver->SolveCorrection(unit, action));
Require(fesa::hht_internal::CheckLinearSolve(sparse, unit, action));
if (!FiniteVector(action))
return {fesa::Matrix(size, size), fesa::Matrix(size, size),
fesa::Matrix(size, size), false};
for (std::size_t row = 0U; row < size; ++row) inverse(row, column) = action[row];
}
fesa::Matrix hbar(size, size);
for (std::size_t row = 0U; row < size; ++row) {
for (std::size_t column = 0U; column < size; ++column) {
RateSumRadius sum;
for (std::size_t middle = 0U; middle < size; ++middle)
sum.Add(products.graph(row, middle) * inverse(middle, column),
ProductRadius(products.graph(row, middle), 0.0,
inverse(middle, column), 0.0));
const double defect = (row == column ? 1.0 : 0.0) - sum.Value();
hbar(row, column) = UpSum(std::abs(defect),
AddRadius(defect, 0.0, sum.Radius()));
}
}
double theta = 0.0;
for (std::size_t row = 0U; row < size; ++row)
theta = std::max(theta, UpperRowSum(hbar, row));
const double lower = std::nextafter(1.0 - theta,
-std::numeric_limits<double>::infinity());
if (!(lower > 0.0) || !std::isfinite(lower))
return {std::move(inverse), fesa::Matrix(size, size), std::move(hbar), false};
fesa::Matrix bound(size, size);
for (std::size_t row = 0U; row < size; ++row) {
double tail = 0.0;
for (std::size_t middle = 0U; middle < size; ++middle)
tail = UpSum(tail, UpProduct(std::abs(inverse(row, middle)),
UpperRowSum(hbar, middle)));
tail = Up(tail / lower);
for (std::size_t column = 0U; column < size; ++column)
bound(row, column) = UpSum(std::abs(inverse(row, column)), tail);
}
return {std::move(inverse), std::move(bound), std::move(hbar), true};
}
void AddPhysicalContractionRadius(const GraphBasisProducts& products,
const fesa::Vector& coordinates,
const fesa::Vector& coordinate_radii,
fesa::Vector* rhs_radius) {
if (coordinates.Size() != products.physical.Columns() ||
coordinate_radii.Size() != coordinates.Size() ||
rhs_radius == nullptr || rhs_radius->Size() != products.physical.Rows())
throw std::runtime_error{"C08 physical graph-radius inventory is invalid."};
for (std::size_t row = 0U; row < products.physical.Rows(); ++row)
for (std::size_t column = 0U; column < products.physical.Columns(); ++column)
(*rhs_radius)[row] = UpSum((*rhs_radius)[row], ProductRadius(
products.physical(row, column), products.physical_radius(row, column),
coordinates[column], coordinate_radii[column]));
}
fesa::Vector BoundGraphSolution(const GraphBasisProducts& products,
const GraphInverseCertificate& inverse,
const fesa::Vector& rhs,
const fesa::Vector& rhs_radius,
const fesa::Vector& actual_center) {
const auto size = products.graph.Rows();
if (!inverse.complete || rhs.Size() != size || rhs_radius.Size() != size ||
actual_center.Size() != size)
throw std::runtime_error{"C08 graph certificate is incomplete."};
if (!FiniteMatrix(products.graph) ||
!FiniteNonnegativeMatrix(products.graph_radius) ||
!FiniteMatrix(inverse.inverse_action) ||
!FiniteNonnegativeMatrix(inverse.inverse_bound) ||
!FiniteNonnegativeMatrix(inverse.residual_operator_bound) ||
!FiniteVector(rhs) || !FiniteNonnegativeVector(rhs_radius) ||
!FiniteVector(actual_center))
throw std::runtime_error{"C08 graph certificate has nonfinite arithmetic input."};
// `actual_center` is the accepted graph/rate solve authority. Do not
// replace it with an independently rounded solve: this certificate bounds
// the actual mapped endpoint, while its N2-L evidence remains separate.
fesa::Vector rbar(size), source(size), y(size), row_a(size);
for (std::size_t row = 0U; row < size; ++row) {
RateSumRadius sum;
for (std::size_t column = 0U; column < size; ++column)
sum.Add(products.graph(row, column) * actual_center[column],
ProductRadius(products.graph(row, column), 0.0, actual_center[column], 0.0));
const double defect = rhs[row] - sum.Value();
rbar[row] = UpSum(std::abs(defect), AddRadius(defect, 0.0, sum.Radius()));
source[row] = UpSum(rbar[row], rhs_radius[row]);
for (std::size_t column = 0U; column < size; ++column)
source[row] = UpSum(source[row],
UpProduct(products.graph_radius(row, column), std::abs(actual_center[column])));
}
for (std::size_t row = 0U; row < size; ++row)
for (std::size_t column = 0U; column < size; ++column)
y[row] = UpSum(y[row], UpProduct(inverse.inverse_bound(row, column),
source[column]));
double theta = 0.0;
for (std::size_t row = 0U; row < size; ++row) {
for (std::size_t middle = 0U; middle < size; ++middle)
for (std::size_t column = 0U; column < size; ++column)
row_a[row] = UpSum(row_a[row], UpProduct(inverse.inverse_bound(row, middle),
products.graph_radius(middle, column)));
theta = std::max(theta, row_a[row]);
}
const double lower = std::nextafter(1.0 - theta,
-std::numeric_limits<double>::infinity());
if (!(lower > 0.0) || !std::isfinite(lower))
throw std::runtime_error{"C08 graph uncertainty feedback is incomplete."};
double norm_y = 0.0;
for (std::size_t row = 0U; row < size; ++row) norm_y = std::max(norm_y, y[row]);
fesa::Vector result(size);
for (std::size_t row = 0U; row < size; ++row)
result[row] = UpSum(y[row], UpProduct(row_a[row], Up(norm_y / lower)));
return result;
}
// N7 arithmetic INDICATOR with binary64 endpoint scalar/derivative operators
// frozen. This is first-order implicit sensitivity, not an enclosure of all
// FEM/AD operator evaluation, nonlinear branch, or equilibrium history error.
// CA selector history is retained separately; no radius is reset or clamped.
ArithmeticRadii GraphArithmeticIndicator(
const Frame& frame, const Frame& old,
const reduced::FixedChart& chart,
const reduced::ConstraintOperators& operators,
const ArithmeticRadii& selector_radii,
const ArithmeticRadii& old_radii, const fesa::HhtParameters& p,
fesa::MklPardisoTangentSolver* solver) {
auto result = selector_radii;
const auto nz = chart.z_columns.size();
if (nz == 0U) return result;
const bool inherited_incomplete =
HasInfiniteRadius(selector_radii.coordinates) ||
HasInfiniteRadius(selector_radii.velocity) ||
HasInfiniteRadius(selector_radii.acceleration) ||
HasInfiniteRadius(selector_radii.algorithmic_acceleration) ||
HasInfiniteRadius(old_radii.coordinates) ||
HasInfiniteRadius(old_radii.velocity) ||
HasInfiniteRadius(old_radii.acceleration) ||
HasInfiniteRadius(old_radii.algorithmic_acceleration);
if (inherited_incomplete) {
// +Inf is an inherited arithmetic bound, not a valid zero uncertainty.
// Retain it as INCONCLUSIVE without attempting max-based graph bounds.
FillInfinite(&result.coordinates);
FillInfinite(&result.velocity);
FillInfinite(&result.acceleration);
FillInfinite(&result.algorithmic_acceleration);
return result;
}
const auto products = ContractGraphBasis(operators, chart);
const auto graph = products.graph;
const auto assembled_graph = reduced::GraphJacobian(operators, chart);
for (std::size_t row = 0U; row < graph.Rows(); ++row)
for (std::size_t column = 0U; column < graph.Columns(); ++column)
if (graph(row, column) != assembled_graph(row, column))
throw std::runtime_error{"C08 graph CA replay differs from factored G."};
const auto inverse = CertifyGraphInverse(products, solver);
if (!inverse.complete)
throw std::runtime_error{"C08 graph inverse-action certificate is incomplete."};
const fesa::Vector zero_z(chart.z_columns.size());
const auto lift_p = [&](const fesa::Vector& coordinates) {
return Require(reduced::Lift(chart, coordinates, zero_z));
};
// Stored C/H/time entries are frozen inputs. The C*Lp/C*Lz replay below
// propagates immutable basis BL and source-order contraction rounding.
const auto p_q_coordinates = Require(reduced::ProjectPhysical(
chart, frame.generalized_coordinates));
const auto p_v_coordinates = Require(reduced::ProjectPhysical(chart, frame.velocity));
const auto p_a_coordinates = Require(reduced::ProjectPhysical(chart, frame.acceleration));
const auto p_q = lift_p(p_q_coordinates);
const auto p_v = lift_p(p_v_coordinates);
const auto p_a = lift_p(p_a_coordinates);
const auto z_q = Require(reduced::ProjectAlgebraic(chart, frame.generalized_coordinates));
const auto z_v = Require(reduced::ProjectAlgebraic(chart, frame.velocity));
const auto z_a = Require(reduced::ProjectAlgebraic(chart, frame.acceleration));
const auto p_coordinate_radii = ProjectBasisRadii(chart.p_columns,
frame.generalized_coordinates, selector_radii.coordinates);
const auto p_velocity_radii = ProjectBasisRadii(chart.p_columns,
frame.velocity, selector_radii.velocity);
const auto p_acceleration_radii = ProjectBasisRadii(chart.p_columns,
frame.acceleration, selector_radii.acceleration);
const auto p_q_full_radius = LiftBasisRadii(chart.p_columns,
p_q_coordinates,
p_coordinate_radii, chart.full_dof_count);
const auto p_v_full_radius = LiftBasisRadii(chart.p_columns,
p_v_coordinates,
p_velocity_radii, chart.full_dof_count);
const auto p_a_full_radius = LiftBasisRadii(chart.p_columns,
p_a_coordinates,
p_acceleration_radii, chart.full_dof_count);
for (std::size_t full = 0U; full < chart.full_dof_count; ++full) {
result.coordinates[full] = AddRadius(p_q[full], selector_radii.coordinates[full],
p_q_full_radius[full]);
result.velocity[full] = AddRadius(p_v[full], selector_radii.velocity[full],
p_v_full_radius[full]);
result.acceleration[full] = AddRadius(p_a[full], selector_radii.acceleration[full],
p_a_full_radius[full]);
}
const auto combine_lift_radius = [&](
const fesa::Vector& target, const fesa::Vector& inherited,
const fesa::Vector& p_coordinates, const fesa::Vector& p_radii,
const fesa::Vector& z_coordinates, const fesa::Vector& z_radii) {
const auto replay = ReplayLiftBasisRadii(chart.p_columns, p_coordinates, p_radii,
chart.z_columns, z_coordinates, z_radii,
chart.full_dof_count);
const auto actual = Require(reduced::Lift(chart, p_coordinates, z_coordinates));
fesa::Vector result(chart.full_dof_count);
for (std::size_t full = 0U; full < chart.full_dof_count; ++full) {
if (actual[full] != replay.center[full])
throw std::runtime_error{"C08 lift radius replay center differs from production Lift."};
const double gap = target[full] - replay.center[full];
const double gap_radius = UpSum(std::abs(gap), RoundoffRadius(gap));
result[full] = AddRadius(target[full], inherited[full],
UpSum(replay.radius[full], gap_radius));
}
return result;
};
const auto rate_defect = [&](const std::size_t row, const bool acceleration) {
const auto& op = operators.rows[row];
RateSumRadius rhs_sum;
if (acceleration) {
RateSumRadius hessian;
for (const auto& entry : op.hessian) {
const double first = entry.value * frame.velocity[entry.row];
hessian.Add(first * frame.velocity[entry.column], ProductRadius(first,
ProductRadius(entry.value, 0.0, frame.velocity[entry.row], 0.0),
frame.velocity[entry.column], 0.0));
}
RateSumRadius time_velocity;
for (const auto& entry : op.time_gradient)
time_velocity.Add(entry.value * frame.velocity[entry.dof],
ProductRadius(entry.value, 0.0, frame.velocity[entry.dof], 0.0));
rhs_sum.Add(-hessian.Value(), hessian.Radius());
rhs_sum.Add(-2.0 * time_velocity.Value(), ProductRadius(
2.0, 0.0, time_velocity.Value(), time_velocity.Radius()));
rhs_sum.Add(-op.time_second, 0.0);
} else {
rhs_sum.Add(-op.time_first, 0.0);
}
const auto& p_channel = acceleration ? p_a : p_v;
for (const auto& entry : op.gradient)
rhs_sum.Add(-entry.value * p_channel[entry.dof],
ProductRadius(entry.value, 0.0, p_channel[entry.dof], 0.0));
const double rhs = rhs_sum.Value();
const double radius = rhs_sum.Radius();
const auto& z_channel = acceleration ? z_a : z_v;
double dot = 0.0;
double dot_radius = 0.0;
for (std::size_t column = 0U; column < nz; ++column) {
if (graph(row, column) == 0.0) continue;
const double term = graph(row, column) * z_channel[column];
dot += term;
dot_radius = AddRadius(dot, dot_radius, ProductRadius(
graph(row, column), 0.0, z_channel[column], 0.0));
}
const double residual = dot - rhs;
return UpSum(std::abs(residual), AddRadius(residual, dot_radius, radius));
};
fesa::Vector position_rhs(nz), velocity_rhs(nz), acceleration_rhs(nz);
for (std::size_t row = 0U; row < nz; ++row) {
position_rhs[row] = UpSum(std::abs(operators.rows[row].value),
RoundoffRadius(operators.rows[row].value));
for (const auto& entry : operators.rows[row].gradient)
position_rhs[row] = UpSum(position_rhs[row],
UpProduct(std::abs(entry.value), result.coordinates[entry.dof]));
}
AddPhysicalContractionRadius(products, Require(reduced::ProjectPhysical(
chart, frame.generalized_coordinates)), p_coordinate_radii, &position_rhs);
const auto bz = BoundGraphSolution(products, inverse, graph.Multiply(z_q),
position_rhs, z_q);
result.coordinates = combine_lift_radius(frame.generalized_coordinates,
selector_radii.coordinates, p_q_coordinates, p_coordinate_radii, z_q, bz);
for (std::size_t row = 0U; row < nz; ++row) {
const auto& op = operators.rows[row];
double first = op.time_first;
double first_radius = 0.0;
for (const auto& entry : op.gradient) {
first += entry.value * frame.velocity[entry.dof];
first_radius = AddRadius(first, first_radius,
ProductRadius(entry.value, 0.0, frame.velocity[entry.dof], 0.0));
velocity_rhs[row] = UpSum(velocity_rhs[row],
UpProduct(std::abs(entry.value), result.velocity[entry.dof]));
}
for (const auto& entry : op.hessian)
velocity_rhs[row] = UpSum(velocity_rhs[row], UpProduct(
UpProduct(std::abs(entry.value), std::abs(frame.velocity[entry.column])),
result.coordinates[entry.row]));
for (const auto& entry : op.time_gradient)
velocity_rhs[row] = UpSum(velocity_rhs[row],
UpProduct(std::abs(entry.value), result.coordinates[entry.dof]));
velocity_rhs[row] = UpSum(velocity_rhs[row],
UpSum(rate_defect(row, false), UpSum(std::abs(first), first_radius)));
}
AddPhysicalContractionRadius(products, Require(reduced::ProjectPhysical(
chart, frame.velocity)), p_velocity_radii, &velocity_rhs);
const auto bvz = BoundGraphSolution(products, inverse, graph.Multiply(z_v),
velocity_rhs, z_v);
result.velocity = combine_lift_radius(frame.velocity, selector_radii.velocity,
p_v_coordinates, p_velocity_radii, z_v, bvz);
for (std::size_t row = 0U; row < nz; ++row) {
const auto& op = operators.rows[row];
double second = 0.0;
double second_radius = 0.0;
for (const auto& entry : op.hessian) {
const double first = entry.value * frame.velocity[entry.row];
second += first * frame.velocity[entry.column];
second_radius = AddRadius(second, second_radius, ProductRadius(first,
ProductRadius(entry.value, 0.0, frame.velocity[entry.row], 0.0),
frame.velocity[entry.column], 0.0));
acceleration_rhs[row] = UpSum(acceleration_rhs[row], UpProduct(
UpProduct(std::abs(entry.value), std::abs(frame.acceleration[entry.column])),
result.coordinates[entry.row]));
acceleration_rhs[row] = UpSum(acceleration_rhs[row], UpProduct(2.0, UpProduct(
UpProduct(std::abs(entry.value), std::abs(frame.velocity[entry.column])),
result.velocity[entry.row])));
}
for (const auto& entry : op.third_directional_gradient)
acceleration_rhs[row] = UpSum(acceleration_rhs[row],
UpProduct(std::abs(entry.value), result.coordinates[entry.dof]));
for (const auto& entry : op.time_gradient) {
const double first = 2.0 * entry.value;
second += first * frame.velocity[entry.dof];
second_radius = AddRadius(second, second_radius, ProductRadius(first,
ProductRadius(2.0, 0.0, entry.value, 0.0), frame.velocity[entry.dof], 0.0));
acceleration_rhs[row] = UpSum(acceleration_rhs[row], UpProduct(2.0,
UpProduct(std::abs(entry.value), result.velocity[entry.dof])));
}
second += op.time_second;
second_radius = AddRadius(second, second_radius, 0.0);
for (const auto& entry : op.gradient) {
second += entry.value * frame.acceleration[entry.dof];
second_radius = AddRadius(second, second_radius,
ProductRadius(entry.value, 0.0, frame.acceleration[entry.dof], 0.0));
acceleration_rhs[row] = UpSum(acceleration_rhs[row],
UpProduct(std::abs(entry.value), result.acceleration[entry.dof]));
}
acceleration_rhs[row] = UpSum(acceleration_rhs[row],
UpSum(rate_defect(row, true), UpSum(std::abs(second), second_radius)));
}
AddPhysicalContractionRadius(products, Require(reduced::ProjectPhysical(
chart, frame.acceleration)), p_acceleration_radii, &acceleration_rhs);
const auto baz = BoundGraphSolution(products, inverse, graph.Multiply(z_a),
acceleration_rhs, z_a);
result.acceleration = combine_lift_radius(frame.acceleration,
selector_radii.acceleration, p_a_coordinates, p_acceleration_radii, z_a, baz);
for (std::size_t i = 0U; i < chart.full_dof_count; ++i) {
result.algorithmic_acceleration[i] = AddRadius(
frame.algorithmic_acceleration[i], ProductRadius(1.0 + p.alpha,
RoundoffRadius(1.0 + p.alpha), frame.acceleration[i], result.acceleration[i]),
ProductRadius(p.alpha, 0.0, old.acceleration[i], old_radii.acceleration[i]));
}
return result;
}
TEST(HhtShellPhysics, FrozenGraphIndicatorIncludesCoordinateAndRateCurvature) {
// Independent polynomial graph z=p^2: vz=2*p*vp,
// az=2*vp^2+2*p*ap. Its first-order radius coefficients are exact.
reduced::FixedChart chart{};
chart.full_dof_count = 2U;
chart.p_dofs = {0U};
chart.z_dofs = {1U};
chart.p_columns = {{0U, 0U, false, {{0U, 1.0}}, {0.0}}};
chart.z_columns = {{0U, 1U, false, {{1U, 1.0}}, {0.0}}};
reduced::ConstraintOperators operators{};
operators.rows.resize(1U);
operators.rows[0].gradient = {{0U, -2.0}, {1U, 1.0}};
operators.rows[0].hessian = {{0U, 0U, -2.0}};
Frame frame{};
frame.generalized_coordinates = fesa::Vector(2U);
frame.velocity = fesa::Vector(2U);
frame.acceleration = fesa::Vector(2U);
frame.algorithmic_acceleration = fesa::Vector(2U);
frame.generalized_coordinates[0U] = frame.generalized_coordinates[1U] = 1.0;
frame.velocity[0U] = 1.0;
frame.velocity[1U] = 2.0;
frame.acceleration[0U] = 2.0;
frame.acceleration[1U] = 6.0;
Frame old = frame;
old.acceleration = fesa::Vector(2U);
const auto parameters = Require(fesa::MakeHhtParameters(-0.05));
for (std::size_t i = 0U; i < 2U; ++i)
frame.algorithmic_acceleration[i] = (1.0 + parameters.alpha) * frame.acceleration[i];
ArithmeticRadii selector{fesa::Vector(2U), fesa::Vector(2U),
fesa::Vector(2U), fesa::Vector(2U)};
const ArithmeticRadii zero = selector;
selector.coordinates[0U] = 1.0e-6;
selector.velocity[0U] = 1.0e-7;
selector.acceleration[0U] = 1.0e-8;
fesa::MklPardisoTangentSolver solver;
const auto indicator = GraphArithmeticIndicator(frame, old, chart, operators,
selector, zero, parameters, &solver);
EXPECT_GE(indicator.coordinates[1U], 2.0e-6);
EXPECT_GE(indicator.velocity[1U], 2.0e-6 + 2.0e-7);
EXPECT_GE(indicator.acceleration[1U], 4.0e-6 + 4.0e-7 + 2.0e-8);
EXPECT_TRUE(std::isfinite(indicator.acceleration[1U]));
EXPECT_GE(indicator.algorithmic_acceleration[1U],
(1.0 + parameters.alpha) * indicator.acceleration[1U]);
selector.coordinates[0U] = std::numeric_limits<double>::infinity();
const auto unresolved = GraphArithmeticIndicator(frame, old, chart, operators,
selector, zero, parameters, &solver);
EXPECT_TRUE(std::isinf(unresolved.coordinates[1U]));
EXPECT_TRUE(std::isinf(unresolved.velocity[1U]));
EXPECT_TRUE(std::isinf(unresolved.acceleration[1U]));
}
TEST(HhtShellPhysics, GraphBasisCertificateBoundsMatrixAndRhsPerturbations) {
const auto solve = [](const fesa::Matrix& matrix, const fesa::Vector& rhs) {
const auto sparse = SparseFromDense(matrix);
fesa::MklPardisoTangentSolver solver;
Require(solver.FactorizeFresh(sparse));
fesa::Vector value(rhs.Size());
Require(solver.SolveCorrection(rhs, value));
Require(fesa::hht_internal::CheckLinearSolve(sparse, rhs, value));
return value;
};
{
GraphBasisProducts products{fesa::Matrix(1U, 1U), fesa::Matrix(1U, 0U),
fesa::Matrix(1U, 1U), fesa::Matrix(1U, 0U)};
products.graph(0U, 0U) = 2.0;
products.graph_radius(0U, 0U) = 0.1;
fesa::MklPardisoTangentSolver solver;
const auto inverse = CertifyGraphInverse(products, &solver);
ASSERT_TRUE(inverse.complete);
const fesa::Vector rhs{1U, 1.0};
const fesa::Vector rhs_radius{1U, 0.01};
const auto center = solve(products.graph, rhs);
const auto bound = BoundGraphSolution(products, inverse, rhs, rhs_radius, center);
auto perturbed = products.graph;
perturbed(0U, 0U) = 1.95;
const auto actual = solve(perturbed, fesa::Vector{1U, 1.009});
EXPECT_LE(std::abs(actual[0U] - center[0U]), bound[0U]);
EXPECT_GT(bound[0U], 0.0);
// The actual mapped graph center is the certificate authority. Replacing
// it with a newly solved center would lose this distance when BG=Bb=0.
products.graph_radius(0U, 0U) = 0.0;
const fesa::Vector shifted_center{1U, 0.5 + std::ldexp(1.0, -30)};
const auto centered = BoundGraphSolution(products, inverse, rhs,
fesa::Vector(1U), shifted_center);
EXPECT_GE(centered[0U], std::ldexp(1.0, -30));
// A zero stored contribution preserves exact zero under an inherited
// +Inf radius, whereas NaN is invalid. The certificate must not let an
// invalid graph radius reach max-based feedback as a silent zero.
EXPECT_EQ(UpProduct(0.0, std::numeric_limits<double>::infinity()), 0.0);
EXPECT_TRUE(std::isinf(UpProduct(0.0, std::numeric_limits<double>::quiet_NaN())));
products.graph_radius(0U, 0U) = std::numeric_limits<double>::infinity();
EXPECT_FALSE(CertifyGraphInverse(products, &solver).complete);
EXPECT_THROW(BoundGraphSolution(products, inverse, rhs,
fesa::Vector(1U), center), std::runtime_error);
}
{
GraphBasisProducts products{fesa::Matrix(4U, 4U), fesa::Matrix(4U, 0U),
fesa::Matrix(4U, 4U), fesa::Matrix(4U, 0U)};
for (std::size_t i = 0U; i < 4U; ++i) {
products.graph(i, i) = 1.0;
products.graph_radius(i, i) = 0.95;
}
fesa::MklPardisoTangentSolver solver;
const auto inverse = CertifyGraphInverse(products, &solver);
ASSERT_TRUE(inverse.complete);
const fesa::Vector rhs(4U, 1.0);
const auto center = solve(products.graph, rhs);
const auto bound = BoundGraphSolution(products, inverse, rhs, fesa::Vector(4U), center);
auto perturbed = products.graph;
for (std::size_t i = 0U; i < 4U; ++i) perturbed(i, i) = 0.1;
const auto actual = solve(perturbed, rhs);
for (std::size_t i = 0U; i < 4U; ++i) {
EXPECT_LE(std::abs(actual[i] - center[i]), bound[i]);
// Without the A feedback term this would be only y=.95 and fail for 9.
EXPECT_GT(bound[i], 9.0);
}
products.graph_radius(0U, 0U) = 1.0;
const auto invalid_inverse = CertifyGraphInverse(products, &solver);
ASSERT_TRUE(invalid_inverse.complete);
EXPECT_THROW(BoundGraphSolution(products, invalid_inverse, rhs,
fesa::Vector(4U), center), std::runtime_error);
}
}
struct LastSolve {
reduced::AugmentedReducedSystem full_system;
reduced::AugmentedSolveSelection selected_system;
fesa::Vector selected_correction;
fesa::Vector full_correction;
};
// Read-only N7 probes retain the actual last Newton matrix, including all
// auxiliary rows. The endpoint RHS includes graph and both hidden defects.
void RecordNoise(const Fixture& fixture, const Assembly& accepted,
const reduced::FixedChart& chart,
const reduced::ConstraintOperators& operators,
const LastSolve& last, const fesa::HhtParameters& p,
const ArithmeticRadii& arithmetic_radii,
fesa::MklPardisoTangentSolver* solver,
GridEvidence* evidence) {
const auto solve_defect = [&](const fesa::SparseMatrix& tangent,
const fesa::Vector& correction,
const fesa::Vector& rhs) {
auto defect = tangent.Multiply(correction);
defect.Axpy(-1.0, rhs);
defect.Scale(-1.0);
fesa::Vector solution(defect.Size());
Require(solver->FactorizeFresh(tangent));
Require(solver->SolveCorrection(defect, solution));
Require(fesa::hht_internal::CheckLinearSolve(tangent, defect, solution));
return solution;
};
const auto selected_defect = solve_defect(
last.selected_system.tangent, last.selected_correction,
last.selected_system.rhs);
const auto selected_full = Require(reduced::EmbedAugmentedSolution(
last.selected_system, selected_defect, last.full_system.rhs.Size()));
fesa::Vector model_full(last.full_system.rhs.Size());
if (last.selected_system.angular_only)
model_full = solve_defect(last.full_system.tangent, last.full_correction,
last.full_system.rhs);
const double cv = p.gamma / (p.beta * evidence->h);
const double ca = 1.0 / ((1.0 + p.alpha) * p.beta * evidence->h * evidence->h);
auto endpoint = Require(reduced::AssembleAugmentedReducedSystem(
accepted, chart, operators, cv, ca, fixture.length, evidence->h));
const auto hessian = reduced::ContractConstraintHessian(
operators, accepted.frame.velocity, accepted.frame.velocity);
for (std::size_t row = 0U; row < chart.z_columns.size(); ++row) {
double first = operators.rows[row].time_first;
double second = hessian[row] + operators.rows[row].time_second;
for (const auto& entry : operators.rows[row].gradient) {
first += entry.value * accepted.frame.velocity[entry.dof];
second += entry.value * accepted.frame.acceleration[entry.dof];
}
for (const auto& entry : operators.rows[row].time_gradient)
second += 2.0 * entry.value * accepted.frame.velocity[entry.dof];
endpoint.rhs[endpoint.physical_count + row] =
-operators.rows[row].value * endpoint.row_scale[endpoint.physical_count + row];
endpoint.rhs[endpoint.physical_count + endpoint.algebraic_count + row] =
-first * endpoint.row_scale[
endpoint.physical_count + endpoint.algebraic_count + row];
endpoint.rhs[endpoint.physical_count + 2U * endpoint.algebraic_count + row] =
-second * endpoint.row_scale[
endpoint.physical_count + 2U * endpoint.algebraic_count + row];
}
fesa::Vector nonlinear_defect(endpoint.rhs.Size());
// The endpoint nonlinear probe uses its freshly assembled actual augmented
// matrix; the linear probe above belongs to the frozen last Newton matrix.
Require(solver->FactorizeFresh(endpoint.tangent));
Require(solver->SolveCorrection(endpoint.rhs, nonlinear_defect));
Require(fesa::hht_internal::CheckLinearSolve(endpoint.tangent, endpoint.rhs,
nonlinear_defect));
const auto last_full = Require(reduced::MapAugmentedCorrection(
chart, last.full_system, last.full_correction, cv, ca));
const auto selected_linear_full = Require(reduced::MapAugmentedCorrection(
chart, last.full_system, selected_full, cv, ca));
std::optional<reduced::AugmentedCorrection> model_linear_full;
if (last.selected_system.angular_only)
model_linear_full = Require(reduced::MapAugmentedCorrection(
chart, last.full_system, model_full, cv, ca));
const auto nonlinear_full = Require(reduced::MapAugmentedCorrection(
chart, endpoint, nonlinear_defect, cv, ca));
const std::array<const fesa::Vector*, 3> radii{
&arithmetic_radii.coordinates, &arithmetic_radii.velocity,
&arithmetic_radii.acceleration};
std::array<fesa::Vector, 3> selector_radii{
LiftBasisRadii(chart.p_columns,
Require(reduced::ProjectPhysical(chart, accepted.frame.generalized_coordinates)),
ProjectBasisRadii(chart.p_columns, accepted.frame.generalized_coordinates,
*radii[0U]), chart.full_dof_count),
LiftBasisRadii(chart.p_columns,
Require(reduced::ProjectPhysical(chart, accepted.frame.velocity)),
ProjectBasisRadii(chart.p_columns, accepted.frame.velocity,
*radii[1U]), chart.full_dof_count),
LiftBasisRadii(chart.p_columns,
Require(reduced::ProjectPhysical(chart, accepted.frame.acceleration)),
ProjectBasisRadii(chart.p_columns, accepted.frame.acceleration,
*radii[2U]), chart.full_dof_count)};
const std::array<const reduced::AugmentedCorrection*, 4> corrections{
&last_full, &selected_linear_full,
model_linear_full.has_value() ? &*model_linear_full : nullptr,
&nonlinear_full};
for (std::size_t node = 0U; node < fixture.domain->Nodes().size(); ++node)
for (std::size_t channel = 0U; channel < 2U; ++channel) {
const auto offset = 6U * node + 3U * channel;
const double scale = channel == 0U ? fixture.length : 1.0;
evidence->full_filter_arithmetic_noise[channel] = std::max(
evidence->full_filter_arithmetic_noise[channel], Up(UpProduct(
evidence->end_time * evidence->end_time,
RadiusNorm(arithmetic_radii.algorithmic_acceleration, offset)) / scale));
for (std::size_t rate = 0U; rate < 3U; ++rate) {
const double time_scale = rate == 0U ? 1.0 : rate == 1U
? evidence->end_time : evidence->end_time * evidence->end_time;
const auto channel_values = [&](const reduced::AugmentedCorrection& value) {
return rate == 0U ? &value.coordinate
: rate == 1U ? &value.velocity : &value.acceleration;
};
double eta = 0.0;
for (const auto* correction : corrections)
if (correction != nullptr)
eta += static_cast<double>(Norm(Triple(*channel_values(*correction),
offset)));
evidence->solve_noise[2U * rate + channel] = std::max(
evidence->solve_noise[2U * rate + channel], time_scale * eta / scale);
evidence->selector_history_arithmetic_noise[2U * rate + channel] = std::max(
evidence->selector_history_arithmetic_noise[2U * rate + channel],
Up(UpProduct(time_scale, RadiusNorm(selector_radii[rate], offset)) / scale));
evidence->arithmetic_noise[2U * rate + channel] = std::max(
evidence->arithmetic_noise[2U * rate + channel], Up(UpProduct(time_scale,
RadiusNorm(*radii[rate], offset)) / scale));
}
}
}
TEST(HhtShellPhysics, RestrictedLastSolvePreservesOmittedTranslationDefect) {
// p0 is translational and p1 is rotational. The restricted solve exactly
// satisfies its angular row, while the omitted U row remains coupled and
// must therefore contribute the separate full-model N7 solve.
reduced::FixedChart chart{};
chart.full_dof_count = 6U;
chart.p_dofs = {0U, 3U};
chart.p_columns = {{0U, 0U, false, {{0U, 1.0}}, {0.0}},
{0U, 1U, false, {{3U, 1.0}}, {0.0}}};
const auto tangent = Require(fesa::hht_internal::MakeSparse(2U, 2U,
{{0U, 0U, 1.0, 0U, 0U}, {0U, 1U, 2.0, 0U, 1U},
{1U, 0U, 3.0, 0U, 2U}, {1U, 1U, 1.0, 0U, 3U}}));
reduced::AugmentedReducedSystem full{tangent, fesa::Vector(2U), 2U, 0U,
fesa::Vector(2U, 1.0),
fesa::Vector(2U, 1.0)};
full.rhs[0U] = 0.0;
full.rhs[1U] = 1.0;
ASSERT_TRUE(reduced::ShouldRefineAngular(chart, {0.0, 1.0},
{0.0, 0.0, 0.0, 0.0}, true));
const auto selected = Require(reduced::SelectAugmentedSolve(full, chart, true));
ASSERT_TRUE(selected.angular_only);
ASSERT_EQ(selected.full_indices, std::vector<std::size_t>({1U}));
fesa::MklPardisoTangentSolver solver;
fesa::Vector angular(selected.rhs.Size());
Require(solver.FactorizeFresh(selected.tangent));
Require(solver.SolveCorrection(selected.rhs, angular));
Require(fesa::hht_internal::CheckLinearSolve(selected.tangent, selected.rhs,
angular));
const auto embedded = Require(reduced::EmbedAugmentedSolution(
selected, angular, full.rhs.Size()));
EXPECT_EQ(embedded[0U], 0.0);
EXPECT_EQ(embedded[1U], 1.0);
const auto selected_residual = selected.tangent.Multiply(angular);
EXPECT_EQ(selected_residual[0U] - selected.rhs[0U], 0.0);
auto full_defect = full.rhs;
full_defect.Axpy(-1.0, full.tangent.Multiply(embedded));
EXPECT_EQ(full_defect[0U], -2.0);
EXPECT_EQ(full_defect[1U], 0.0);
fesa::Vector full_model(full_defect.Size());
Require(solver.FactorizeFresh(full.tangent));
Require(solver.SolveCorrection(full_defect, full_model));
Require(fesa::hht_internal::CheckLinearSolve(full.tangent, full_defect,
full_model));
EXPECT_GT(std::abs(full_model[0U]), 0.0);
EXPECT_GT(std::abs(full_model[1U]), 0.0);
}
TEST(HhtShellPhysics, RestrictedRecordNoiseIncludesFullModelOmittedUDefect) {
// Keep the actual shell accepted frame/chart/hidden endpoint rows. The
// frozen last operator is a minimal full-size coupled matrix: its J rows
// are exactly solved while one omitted translational U row retains a
// nonzero full-model defect.
const auto fixture = MakeFixture(1U);
constexpr double h = 1.0 / 64.0;
const auto parameters = Require(fesa::MakeHhtParameters(-0.05));
const auto accepted = Evaluate(fixture, EmptyFrame(fixture), h, parameters, true);
const auto chart = Require(reduced::BuildFixedChart(fixture.dofs,
accepted.rotations));
const fesa::Vector zero(fixture.dofs.FullDofCount());
const auto operators = Require(reduced::EvaluateConstraintOperators(
*fixture.domain, fixture.dofs, fixture.shells, accepted, chart, zero, zero));
const double cv = parameters.gamma / (parameters.beta * h);
const double ca = 1.0 / ((1.0 + parameters.alpha) * parameters.beta * h * h);
const std::size_t total = chart.p_columns.size() + 3U * chart.z_columns.size();
std::size_t u = chart.p_columns.size();
std::size_t angular = chart.p_columns.size();
for (std::size_t i = 0U; i < chart.p_columns.size(); ++i) {
if (reduced::PhysicalCoordinateChannel(chart, i) == 0U) u = i;
else angular = i;
}
ASSERT_LT(u, chart.p_columns.size());
ASSERT_LT(angular, chart.p_columns.size());
std::vector<fesa::CooContribution> entries;
entries.reserve(total + 2U);
for (std::size_t i = 0U; i < total; ++i)
entries.push_back({i, i, 1.0, 0U, entries.size()});
entries.push_back({u, angular, 2.0, 0U, entries.size()});
entries.push_back({angular, u, 3.0, 0U, entries.size()});
const auto synthetic_tangent = Require(fesa::hht_internal::MakeSparse(
total, total, std::move(entries)));
reduced::AugmentedReducedSystem last_system{
synthetic_tangent, fesa::Vector(total), chart.p_columns.size(),
chart.z_columns.size(), fesa::Vector(total, 1.0), fesa::Vector(total, 1.0)};
fesa::Vector full_correction(total);
full_correction[angular] = 1.0;
last_system.rhs = last_system.tangent.Multiply(full_correction);
const auto baseline_selected = Require(reduced::SelectAugmentedSolve(
last_system, chart, true));
fesa::Vector selected_correction(baseline_selected.rhs.Size());
for (std::size_t i = 0U; i < selected_correction.Size(); ++i)
selected_correction[i] = full_correction[baseline_selected.full_indices[i]];
const LastSolve baseline{last_system, baseline_selected, selected_correction,
full_correction};
auto restricted_system = last_system;
restricted_system.rhs[u] = 0.0;
const auto restricted_selected = Require(reduced::SelectAugmentedSolve(
restricted_system, chart, true));
ASSERT_EQ(restricted_selected.full_indices, baseline_selected.full_indices);
const LastSolve restricted{restricted_system, restricted_selected,
selected_correction, full_correction};
auto full_defect = restricted_system.rhs;
full_defect.Axpy(-1.0, restricted_system.tangent.Multiply(full_correction));
ASSERT_NE(full_defect[u], 0.0);
fesa::MklPardisoTangentSolver solver;
fesa::Vector full_model(full_defect.Size());
Require(solver.FactorizeFresh(restricted_system.tangent));
Require(solver.SolveCorrection(full_defect, full_model));
Require(fesa::hht_internal::CheckLinearSolve(restricted_system.tangent,
full_defect, full_model));
const auto mapped_model = Require(reduced::MapAugmentedCorrection(
chart, restricted_system, full_model, cv, ca));
EXPECT_GT(mapped_model.coordinate.Norm(), 0.0);
const ArithmeticRadii radii{zero, zero, zero, zero};
GridEvidence baseline_evidence{h, h};
GridEvidence restricted_evidence{h, h};
RecordNoise(fixture, accepted, chart, operators, baseline, parameters,
radii, &solver, &baseline_evidence);
RecordNoise(fixture, accepted, chart, operators, restricted, parameters,
radii, &solver, &restricted_evidence);
double baseline_noise = 0.0;
double restricted_noise = 0.0;
for (std::size_t i = 0U; i < baseline_evidence.solve_noise.size(); ++i) {
baseline_noise += baseline_evidence.solve_noise[i];
restricted_noise += restricted_evidence.solve_noise[i];
}
EXPECT_GT(restricted_noise, baseline_noise);
}
GridEvidence::RecoveredAcceleration RecoverAcceleration(
const Fixture& fixture, const Assembly& assembly,
fesa::MklPardisoTangentSolver* solver, const bool manufactured) {
const auto nodes = assembly.rotations.size();
GridEvidence::RecoveredAcceleration result{assembly.frame.acceleration};
std::vector<double> weights(nodes);
std::vector<fesa::CooContribution> entries;
fesa::Vector rhs(nodes);
for (std::size_t row = 0; row < nodes; ++row) {
const auto& rotation = assembly.rotations[row];
fesa::Vector null(assembly.frame.acceleration.Size());
for (std::size_t c = 0; c < 3U; ++c)
null[6U * row + 3U + c] = rotation.drill_null[c];
const auto metric_null = assembly.drilling_metric.Multiply(null);
const double stiffness = null.Dot(metric_null);
if (!std::isfinite(stiffness) || !(stiffness > 0.0))
throw std::runtime_error{"C08 recovery requires positive null metric."};
weights[row] = std::sqrt(stiffness);
}
for (std::size_t row = 0; row < nodes; ++row) {
const auto& rotation = assembly.rotations[row];
const double raw = assembly.frame.hidden_acceleration_defect[row];
const double raw_scale =
assembly.frame.hidden_acceleration_defect_scale[row];
result.pre_hidden2 = std::max(result.pre_hidden2, std::abs(raw));
result.pre_hidden2_relative =
std::max(result.pre_hidden2_relative,
raw_scale == 0.0 ? std::abs(raw) : std::abs(raw) / raw_scale);
rhs[row] = -raw / weights[row];
for (std::size_t c = 0; c < 3U; ++c) {
const auto full = 6U * row + 3U + c;
const double force = assembly.frame.internal_force[full] -
assembly.frame.external_force[full];
for (std::size_t d = 0; d < 3U; ++d)
entries.push_back({row, row,
rotation.null_gradient(c, d) * force *
rotation.drill_null[d] / weights[row] /
weights[row],
0U, entries.size()});
for (std::size_t index = assembly.internal_tangent.RowOffsets()[full];
index < assembly.internal_tangent.RowOffsets()[full + 1U]; ++index) {
const auto column = assembly.internal_tangent.ColumnIndices()[index];
if (column % 6U < 3U) continue;
const auto column_node = column / 6U;
entries.push_back(
{row, column_node,
rotation.drill_null[c] *
assembly.internal_tangent.Values()[index] *
assembly.rotations[column_node].drill_null[column % 6U - 3U] /
weights[row] / weights[column_node],
0U, entries.size()});
}
}
}
const auto regularity =
Require(fesa::hht_internal::MakeSparse(nodes, nodes, std::move(entries)));
fesa::Vector scaled_eta(nodes);
Require(solver->FactorizeFresh(regularity));
Require(solver->SolveCorrection(rhs, scaled_eta));
Require(fesa::hht_internal::CheckLinearSolve(regularity, rhs, scaled_eta));
fesa::Vector delta(result.value.Size());
for (std::size_t node = 0; node < nodes; ++node)
for (std::size_t c = 0; c < 3U; ++c)
delta[6U * node + 3U + c] = assembly.rotations[node].drill_null[c] *
scaled_eta[node] / weights[node];
result.value.Axpy(1.0, delta);
const auto mass_delta = assembly.mass.Multiply(delta);
const double mass_scale = SparseNorm(assembly.mass) * delta.Norm();
result.mass_delta_a_relative =
mass_scale == 0.0 ? mass_delta.Norm() : mass_delta.Norm() / mass_scale;
if (manufactured) {
auto post = assembly.frame;
post.acceleration = result.value;
ManufacturedHidden(assembly, RigidForceRates(fixture, post.time), &post);
for (std::size_t row = 0; row < nodes; ++row) {
const double value = std::abs(post.hidden_acceleration_defect[row]);
const double scale = post.hidden_acceleration_defect_scale[row];
result.post_hidden2 = std::max(result.post_hidden2, value);
result.post_hidden2_relative = std::max(
result.post_hidden2_relative, scale == 0.0 ? value : value / scale);
}
return result;
}
for (std::size_t row = 0; row < nodes; ++row) {
fesa::Vector gradient(result.value.Size());
const auto& rotation = assembly.rotations[row];
for (std::size_t c = 0; c < 3U; ++c) {
const auto full = 6U * row + 3U + c;
const double force = assembly.frame.internal_force[full] -
assembly.frame.external_force[full];
for (std::size_t d = 0; d < 3U; ++d) {
const auto column = 6U * row + 3U + d;
gradient[column] += rotation.null_gradient(c, d) * force;
}
for (std::size_t index = assembly.internal_tangent.RowOffsets()[full];
index < assembly.internal_tangent.RowOffsets()[full + 1U]; ++index) {
const auto column = assembly.internal_tangent.ColumnIndices()[index];
gradient[column] +=
rotation.drill_null[c] * assembly.internal_tangent.Values()[index];
}
}
const double post =
assembly.hidden_second[row] + gradient.Dot(result.value);
double post_scale = assembly.hidden_second_scale[row];
for (std::size_t column = 0; column < gradient.Size(); ++column)
post_scale += std::abs(gradient[column] * result.value[column]);
result.post_hidden2 = std::max(result.post_hidden2, std::abs(post));
result.post_hidden2_relative = std::max(
result.post_hidden2_relative,
post_scale == 0.0 ? std::abs(post) : std::abs(post) / post_scale);
}
return result;
}
void CompleteOriginalConstraints(const Fixture& fixture, const Assembly& assembly,
const std::vector<std::size_t>& null_nodes,
fesa::MklPardisoSolver& positive,
fesa::MklPardisoTangentSolver& tangent,
Frame* frame, const double h, const char* stage,
const reduced::EmbeddingRoot* root = nullptr) {
const auto status = fesa::hht_internal::CompleteConstraintEvidence(
*fixture.domain, fixture.dofs, assembly, fixture.length, null_nodes,
positive, tangent, frame);
if (status.IsOk()) return;
std::ostringstream context;
context << std::setprecision(17) << "C08 h=" << h
<< ", increment=" << frame->increment_index << ", time=" << frame->time
<< ", stage=" << stage << ": " << Message(status);
if (root != nullptr)
context << "; graph_iterations=" << root->graph_iteration_count
<< ", graph_residual=" << root->graph_residual_infinity
<< ", graph_relative=" << root->graph_residual_relative
<< ", graph_correction=" << root->graph_correction_infinity;
std::cout << std::setprecision(17) << "C08_HIDDEN_Q=";
for (std::size_t i = 0U; i < frame->generalized_coordinates.Size(); ++i)
std::cout << frame->generalized_coordinates[i] << ',';
std::cout << "\n";
const auto chart = reduced::BuildFixedChart(fixture.dofs, assembly.rotations);
if (chart.HasValue()) {
const fesa::Vector zero(fixture.dofs.FullDofCount());
const auto operators = reduced::EvaluateConstraintOperators(
*fixture.domain, fixture.dofs, fixture.shells, assembly, chart.Value(), zero, zero);
if (operators.HasValue()) {
for (std::size_t row = 0U; row < chart.Value().null_nodes.size(); ++row) {
const auto node = chart.Value().null_nodes[row];
double projected = 0.0, contraction_scale = 0.0, residual_scale = 0.0;
for (std::size_t c = 0U; c < 3U; ++c) {
const auto full = 6U * node + 3U + c;
const double n = assembly.rotations[node].drill_null[c];
const double term = n * (frame->internal_force[full] - frame->external_force[full]);
projected += term;
contraction_scale += std::abs(term);
residual_scale += std::abs(n) * frame->residual_scale[full];
std::cout << std::setprecision(17) << "C08_HIDDEN_COMPONENT h=" << h
<< " increment=" << frame->increment_index << " node=" << node
<< " full=" << full << " n=" << n
<< " internal=" << frame->internal_force[full]
<< " external=" << frame->external_force[full]
<< " inertia=" << frame->inertia_force[full]
<< " q=" << frame->generalized_coordinates[full] << "\n";
}
std::cout << std::setprecision(17) << "C08_HIDDEN_FAILURE h=" << h
<< " increment=" << frame->increment_index << " time=" << frame->time
<< " stage=" << stage << " node=" << node
<< " raw_psi=" << operators.Value().rows[row].value
<< " raw_scale=" << operators.Value().rows[row].value_scale
<< " projected_psi=" << projected << " contraction_scale=" << contraction_scale
<< " residual_scale=" << residual_scale
<< " difference=" << projected - operators.Value().rows[row].value << "\n";
}
}
}
throw std::runtime_error{context.str()};
}
GridEvidence RunPlate(const Fixture& fixture, const double h,
const bool manufactured) {
GridEvidence evidence{h, manufactured ? 1.0 : kEndTime};
const auto p = Require(fesa::MakeHhtParameters(-0.05));
fesa::MklPardisoSolver positive;
fesa::MklPardisoTangentSolver tangent;
Frame frame = manufactured ? RigidOrbit(fixture, 0.0) : EmptyFrame(fixture);
const L3 translation{0.125L, -0.25L, 0.0625L};
const L3 omega{0.8L, 0.6L, 0.4L};
if (manufactured) {
frame.external_force = Integrate(fixture, frame).force;
} else {
for (std::size_t node = 0; node < fixture.domain->Nodes().size(); ++node) {
const auto x = Coordinates(fixture.domain->Nodes()[node]);
L3 velocity = Add(translation, Cross(omega, x));
velocity[2] += 0.02L * x[0] * x[1];
Store(velocity, &frame.velocity, 6U * node);
Store({0.8L, 0.6L, 0.0L}, &frame.velocity, 6U * node + 3U);
}
}
auto assembly = Evaluate(fixture, frame, h, p, true);
if (!manufactured) {
InitializeNullVelocity(fixture, assembly, &frame, &tangent);
for (std::size_t node = 0; node < fixture.domain->Nodes().size(); ++node)
EXPECT_NEAR(frame.velocity[6U * node + 5U], 0.4, 1.0e-12);
assembly = Evaluate(fixture, frame, h, p, true);
}
const auto null_nodes =
fesa::hht_internal::NullNodes(fixture.dofs, assembly.rotations);
if (!manufactured)
frame.acceleration = Require(fesa::hht_internal::InitializeAcceleration(
*fixture.domain, fixture.dofs, assembly, fixture.length, positive,
tangent));
frame.algorithmic_acceleration = frame.acceleration;
frame.time_integration_formulation =
fesa::DynamicTimeIntegrationFormulation::kReducedCoordinateHht;
assembly = Evaluate(fixture, frame, h, p, true);
frame = assembly.frame;
// The fresh physical assembly certifies the initialized acceleration before
// it becomes frame-zero history; the initializer's private solve is separate.
for (const double residual : frame.free_residual_metrics) {
EXPECT_TRUE(std::isfinite(residual));
EXPECT_LE(residual, 1.0e-10);
}
if (manufactured)
CompleteManufacturedConstraints(fixture, assembly, null_nodes, &positive,
&tangent, &frame);
else
CompleteOriginalConstraints(fixture, assembly, null_nodes, positive, tangent,
&frame, h, "initial");
Require(fesa::hht_internal::CompleteEquilibrium(*fixture.domain, fixture.dofs,
assembly, &frame));
const auto initial_oracle = Integrate(fixture, frame);
const double energy0 = frame.kinetic_energy + frame.physical_strain_energy +
frame.drilling_energy;
EXPECT_GT(energy0, 0.0);
EXPECT_LE(std::abs(frame.kinetic_energy -
static_cast<double>(initial_oracle.kinetic)) /
energy0,
1.0e-12);
const auto initial_momentum = frame.linear_momentum;
const auto initial_angular_momentum = frame.angular_momentum;
evidence.history.push_back(frame);
evidence.recovered_acceleration.push_back({frame.acceleration});
ArithmeticRadii arithmetic_radii{fesa::Vector(frame.generalized_coordinates.Size()),
fesa::Vector(frame.generalized_coordinates.Size()),
fesa::Vector(frame.generalized_coordinates.Size()),
fesa::Vector(frame.generalized_coordinates.Size())};
const auto chart = Require(reduced::BuildFixedChart(fixture.dofs, assembly.rotations));
fesa::Vector private_p = Require(reduced::ProjectPhysical(
chart, frame.generalized_coordinates));
fesa::Vector private_v = Require(reduced::ProjectPhysical(chart, frame.velocity));
fesa::Vector private_a = Require(reduced::ProjectPhysical(chart, frame.acceleration));
fesa::Vector private_ar = private_a;
fesa::Vector private_p_radius = ProjectBasisRadii(
chart.p_columns, frame.generalized_coordinates, arithmetic_radii.coordinates);
fesa::Vector private_v_radius = ProjectBasisRadii(
chart.p_columns, frame.velocity, arithmetic_radii.velocity);
fesa::Vector private_a_radius = ProjectBasisRadii(
chart.p_columns, frame.acceleration, arithmetic_radii.acceleration);
fesa::Vector private_ar_radius = private_a_radius;
fesa::Vector seed = Require(reduced::ProjectAlgebraic(
chart, frame.generalized_coordinates));
const fesa::Vector zero(fixture.dofs.FullDofCount());
const auto force_rates = [&](const double time) {
return manufactured ? RigidForceRates(fixture, time)
: std::array<fesa::Vector, 2>{zero, zero};
};
const auto position_operators = [&](const Frame& endpoint,
const fesa::Vector& q) {
auto position = endpoint;
position.generalized_coordinates = q;
position.velocity = zero;
position.acceleration = zero;
position.algorithmic_acceleration = zero;
const auto position_assembly = Evaluate(fixture, position, h, p, false);
const auto rates = force_rates(endpoint.time);
return reduced::EvaluateConstraintOperators(*fixture.domain, fixture.dofs,
fixture.shells, position_assembly, chart, rates[0], rates[1]);
};
const auto root0 = Require(reduced::SolveEmbeddingRoot(chart, private_p, seed,
[&](const fesa::Vector& q) { return position_operators(frame, q); }, tangent));
const auto rates0 = Require(reduced::ReconstructPhysicalRates(
chart, root0, private_v, private_a, tangent));
for (std::size_t i = 0U; i < zero.Size(); ++i) {
EXPECT_DOUBLE_EQ(root0.q[i], frame.generalized_coordinates[i]);
EXPECT_DOUBLE_EQ(rates0.velocity[i], frame.velocity[i]);
}
// Preserve the original frame zero. Certify independently reconstructed a0
// by the original physical equation and N3/N4 gates, without a component
// absolute floor or silently projecting the stored initial state.
auto reconstructed_zero = frame;
reconstructed_zero.generalized_coordinates = root0.q;
reconstructed_zero.velocity = rates0.velocity;
reconstructed_zero.acceleration = rates0.acceleration;
reconstructed_zero.algorithmic_acceleration = rates0.acceleration;
const auto reconstructed_assembly = Evaluate(fixture, reconstructed_zero, h, p, true);
reconstructed_zero = reconstructed_assembly.frame;
if (manufactured)
CompleteManufacturedConstraints(fixture, reconstructed_assembly, null_nodes,
&positive, &tangent, &reconstructed_zero);
else
CompleteOriginalConstraints(fixture, reconstructed_assembly, null_nodes,
positive, tangent, &reconstructed_zero, h,
"reconstructed-initial");
Require(fesa::hht_internal::CompleteEquilibrium(*fixture.domain, fixture.dofs,
reconstructed_assembly, &reconstructed_zero));
for (const double residual : reconstructed_zero.free_residual_metrics)
EXPECT_LE(residual, 1.0e-10);
Require(reduced::CheckGraphRegularity(assembly, chart, root0.operators, tangent));
const auto increments = static_cast<std::uint32_t>(evidence.end_time / h);
for (std::uint32_t increment = 1U; increment <= increments; ++increment) {
const Frame old = frame;
// FB-08 is private-coordinate history. Basis labels must never be
// reinterpreted as full indices or be fed through a full-vector predictor.
fesa::HhtPredictor predictor{fesa::Vector(private_p.Size()),
fesa::Vector(private_p.Size())};
fesa::Vector predictor_coordinate_radius(private_p.Size());
fesa::Vector predictor_velocity_radius(private_p.Size());
for (std::size_t i = 0U; i < private_p.Size(); ++i) {
const double velocity_term = h * private_v[i];
const double h2 = h * h;
const double acceleration_term = h2 * (0.5 - p.beta) * private_ar[i];
predictor.coordinates[i] = private_p[i] + velocity_term + acceleration_term;
predictor.velocity[i] = private_v[i] + h * (1.0 - p.gamma) * private_ar[i];
const double velocity_radius = ProductRadius(h, 0.0,
private_v[i], private_v_radius[i]);
const double h2_radius = ProductRadius(h, 0.0, h, 0.0);
const double acceleration_radius = ProductRadius(
h2 * (0.5 - p.beta),
ProductRadius(h2, h2_radius, 0.5 - p.beta,
RoundoffRadius(0.5 - p.beta)),
private_ar[i], private_ar_radius[i]);
predictor_coordinate_radius[i] = AddRadius(predictor.coordinates[i],
AddRadius(private_p[i] + velocity_term, private_p_radius[i], velocity_radius),
acceleration_radius);
const double rate_factor = h * (1.0 - p.gamma);
predictor_velocity_radius[i] = AddRadius(predictor.velocity[i],
private_v_radius[i], ProductRadius(rate_factor,
ProductRadius(h, 0.0, 1.0 - p.gamma,
RoundoffRadius(1.0 - p.gamma)),
private_ar[i], private_ar_radius[i]));
}
frame.is_initial = false;
frame.frame_index = increment;
frame.increment_index = increment;
frame.time = static_cast<double>(increment) * h;
frame.time_integration_formulation =
fesa::DynamicTimeIntegrationFormulation::kReducedCoordinateHht;
if (manufactured)
frame.external_force = Integrate(fixture, RigidOrbit(fixture, frame.time)).force;
bool correction_pass = true;
// These are the last *applied* translation-channel corrections. They
// deliberately remain outside the all-channel acceptance flag: the
// approved restricted solve is eligible only after this channel itself
// has passed its original four mapped-update checks.
std::array<double, 4U> translation_corrections{};
bool translation_private_algorithmic_pass = true;
const auto private_algorithmic_equal = [](const double actual,
const double reduced_value) {
const double defect = std::abs(actual - reduced_value);
const double scale = std::abs(reduced_value);
return std::isfinite(defect) && std::isfinite(scale) &&
(scale == 0.0 ? defect == 0.0 : defect / scale <= 1.0e-12);
};
bool converged = false;
std::optional<LastSolve> last;
fesa::Vector ar(chart.p_columns.size());
fesa::Vector ar_radius(chart.p_columns.size());
fesa::Vector candidate_p(chart.p_columns.size());
fesa::Vector candidate_p_radius(chart.p_columns.size());
fesa::Vector candidate_p_low(chart.p_columns.size());
fesa::Vector candidate_p_pair_radius(chart.p_columns.size());
fesa::Vector chosen_start_p(chart.p_columns.size());
fesa::Vector last_p_before_update(chart.p_columns.size());
fesa::Vector last_requested_delta(chart.p_columns.size());
fesa::Vector addition_low_sum(chart.p_columns.size());
fesa::Vector requested_sum(chart.p_columns.size());
fesa::Vector requested_compensation(chart.p_columns.size());
fesa::Vector requested_absolute_sum(chart.p_columns.size());
bool copy_translation_rates = false;
fesa::Vector retained_velocity(chart.p_columns.size());
fesa::Vector retained_acceleration(chart.p_columns.size());
fesa::Vector retained_velocity_radius(chart.p_columns.size());
fesa::Vector retained_acceleration_radius(chart.p_columns.size());
const auto record_requested_increment = [&](const std::size_t i, const double delta) {
const double previous = requested_sum[i];
const double next = previous + delta;
requested_compensation[i] += std::abs(previous) >= std::abs(delta)
? (previous - next) + delta : (delta - next) + previous;
requested_sum[i] = next;
if (delta != 0.0)
requested_absolute_sum[i] = UpSum(requested_absolute_sum[i], std::abs(delta));
};
for (std::size_t i = 0U; i < candidate_p.Size(); ++i) {
candidate_p[i] = predictor.coordinates[i] + p.beta * h * h * ar[i];
chosen_start_p[i] = candidate_p[i];
const double velocity_term = h * private_v[i];
const double acceleration_term = h * h * (0.5 - p.beta) * private_ar[i];
const double first_sum = private_p[i] + velocity_term;
addition_low_sum[i] = AdditionLowPart(private_p[i], velocity_term, first_sum) +
AdditionLowPart(first_sum, acceleration_term, predictor.coordinates[i]);
candidate_p_low[i] = addition_low_sum[i];
record_requested_increment(i, h * private_v[i]);
record_requested_increment(i, h * h * (0.5 - p.beta) * private_ar[i]);
candidate_p_radius[i] = AddRadius(candidate_p[i],
predictor_coordinate_radius[i], ProductRadius(p.beta * h * h,
HhtCoordinateFactorRadius(p, h), ar[i], ar_radius[i]));
candidate_p_pair_radius[i] = UpSum(candidate_p_radius[i],
std::abs(candidate_p_low[i]));
}
ArithmeticRadii trial_radii{zero, zero, zero, zero};
struct Trial {
Assembly assembly;
reduced::EmbeddingRoot root;
fesa::Vector vp;
fesa::Vector ap;
fesa::Vector ar;
};
const auto evaluate_trial = [&](const fesa::Vector& trial_ar,
const fesa::Vector& physical_p) {
fesa::Vector vp(chart.p_columns.size());
fesa::Vector ap(chart.p_columns.size());
for (std::size_t i = 0U; i < physical_p.Size(); ++i) {
if (copy_translation_rates && reduced::PhysicalCoordinateChannel(chart, i) == 0U) {
vp[i] = retained_velocity[i];
ap[i] = retained_acceleration[i];
} else {
vp[i] = predictor.velocity[i] + p.gamma * h * trial_ar[i];
ap[i] = (trial_ar[i] + p.alpha * private_a[i]) / (1.0 + p.alpha);
}
}
auto root = Require(reduced::SolveEmbeddingRoot(chart, physical_p, seed,
[&](const fesa::Vector& q) { return position_operators(frame, q); }, tangent));
const auto rates = Require(reduced::ReconstructPhysicalRates(
chart, root, vp, ap, tangent));
auto candidate = frame;
candidate.generalized_coordinates = root.q;
candidate.velocity = rates.velocity;
candidate.acceleration = rates.acceleration;
for (std::size_t i = 0U; i < zero.Size(); ++i)
candidate.algorithmic_acceleration[i] =
(1.0 + p.alpha) * candidate.acceleration[i] - p.alpha * old.acceleration[i];
return Trial{Evaluate(fixture, candidate, h, p, true), std::move(root),
std::move(vp), std::move(ap), trial_ar};
};
auto trial = evaluate_trial(ar, candidate_p);
bool same_start = true;
fesa::Vector old_ar(ar.Size());
for (std::size_t i = 0U; i < ar.Size(); ++i) {
same_start = same_start && private_p[i] == predictor.coordinates[i];
old_ar[i] = (private_p[i] - predictor.coordinates[i]) / (p.beta * h * h);
}
if (!same_start) {
// Both candidates are lifted at the new endpoint time; cache the winner.
const auto old_trial = evaluate_trial(old_ar, private_p);
const auto residual_norm = [&](const Assembly& value) {
double norm = 0.0;
for (const auto full : fixture.dofs.FreeDofs())
norm = std::max(norm, std::abs(value.frame.residual[full]) *
(full % 6U < 3U ? fixture.length : 1.0));
if (!std::isfinite(norm)) throw std::runtime_error{"C08 nonfinite start residual."};
return norm;
};
if (residual_norm(old_trial.assembly) < residual_norm(trial.assembly)) {
trial = old_trial;
ar = old_ar;
candidate_p = private_p;
chosen_start_p = private_p;
candidate_p_low = fesa::Vector(ar.Size());
addition_low_sum = fesa::Vector(ar.Size());
candidate_p_radius = private_p_radius;
for (std::size_t i = 0U; i < ar.Size(); ++i)
candidate_p_pair_radius[i] = candidate_p_radius[i];
// The selected old-p object has exact zero initial displacement.
requested_sum = fesa::Vector(ar.Size());
requested_compensation = fesa::Vector(ar.Size());
requested_absolute_sum = fesa::Vector(ar.Size());
for (std::size_t i = 0U; i < ar.Size(); ++i) {
const double numerator = private_p[i] - predictor.coordinates[i];
ar_radius[i] = QuotientRadius(numerator,
AddRadius(numerator, private_p_radius[i],
predictor_coordinate_radius[i]),
p.beta * h * h, HhtCoordinateFactorRadius(p, h));
}
}
}
for (std::uint32_t iteration = 0U; iteration <= 100U; ++iteration) {
if (iteration > 0U) trial = evaluate_trial(ar, candidate_p);
assembly = trial.assembly;
frame = assembly.frame;
const auto time_rates = force_rates(frame.time);
const auto operators = Require(reduced::EvaluateConstraintOperators(
*fixture.domain, fixture.dofs, fixture.shells, assembly, chart,
time_rates[0], time_rates[1]));
// The trial radii begin with the immutable sparse p basis. The graph
// indicator appends z/vz/az uncertainty after its frozen endpoint solve.
fesa::Vector vp_radius(chart.p_columns.size());
fesa::Vector ap_radius(chart.p_columns.size());
for (std::size_t i = 0U; i < ar.Size(); ++i) {
if (copy_translation_rates &&
reduced::PhysicalCoordinateChannel(chart, i) == 0U) {
vp_radius[i] = retained_velocity_radius[i];
ap_radius[i] = retained_acceleration_radius[i];
continue;
}
vp_radius[i] = AddRadius(trial.vp[i], predictor_velocity_radius[i],
ProductRadius(p.gamma * h, ProductRadius(p.gamma, 0.0, h, 0.0),
ar[i], ar_radius[i]));
const double numerator = ar[i] + p.alpha * private_a[i];
ap_radius[i] = QuotientRadius(numerator,
AddRadius(numerator, ar_radius[i], ProductRadius(p.alpha, 0.0,
private_a[i], private_a_radius[i])),
1.0 + p.alpha, RoundoffRadius(1.0 + p.alpha));
}
trial_radii = {LiftBasisRadii(chart.p_columns, candidate_p,
candidate_p_radius, chart.full_dof_count),
LiftBasisRadii(chart.p_columns, trial.vp, vp_radius,
chart.full_dof_count),
LiftBasisRadii(chart.p_columns, trial.ap, ap_radius,
chart.full_dof_count),
LiftBasisRadii(chart.p_columns, ar, ar_radius,
chart.full_dof_count)};
if (correction_pass && frame.free_residual_metrics[0] <= 1.0e-10 &&
frame.free_residual_metrics[1] <= 1.0e-10) {
if (manufactured)
CompleteManufacturedConstraints(fixture, assembly, null_nodes,
&positive, &tangent, &frame);
else
CompleteOriginalConstraints(fixture, assembly, null_nodes, positive,
tangent, &frame, h, "accepted", &trial.root);
Require(fesa::hht_internal::CompleteEquilibrium(*fixture.domain,
fixture.dofs, assembly, &frame));
Require(reduced::CheckGraphRegularity(assembly, chart, operators, tangent));
if (!last) throw std::runtime_error{"C08 nontrivial step lacks N7 last solve."};
const auto full_indicator = GraphArithmeticIndicator(frame, old, chart,
operators, trial_radii, arithmetic_radii, p, &tangent);
RecordNoise(fixture, assembly, chart, operators, *last, p, full_indicator,
&tangent, &evidence);
auto recovery_assembly = assembly;
recovery_assembly.frame = frame;
evidence.recovered_acceleration.push_back(RecoverAcceleration(
fixture, recovery_assembly, &tangent, manufactured));
frame.iteration_count = iteration;
const auto update_equal = [](const double actual,
const std::initializer_list<double> terms) {
double value = 0.0;
double scale = 0.0;
for (const double term : terms) {
value += term;
scale += std::abs(term);
}
const double defect = std::abs(actual - value);
return std::isfinite(defect) && std::isfinite(scale) &&
(scale == 0.0 ? defect == 0.0 : defect / scale <= 1.0e-12);
};
struct UpdateTrace {
double ratio = 0.0;
double actual = 0.0;
double expected = 0.0;
double scale = 0.0;
std::size_t full = 0U;
double signed_sum = 0.0;
double compensation = 0.0;
};
std::array<UpdateTrace, 3> update_trace{};
const auto trace_update = [&](const std::size_t check, const std::size_t full,
const double actual,
const std::initializer_list<double> terms) {
double expected = 0.0;
double scale = 0.0;
for (const double term : terms) {
expected += term;
scale += std::abs(term);
}
const double defect = std::abs(actual - expected);
const double ratio = scale == 0.0
? (defect == 0.0 ? 0.0 : std::numeric_limits<double>::infinity())
: defect / scale;
if (ratio >= update_trace[check].ratio)
update_trace[check] = {ratio, actual, expected, scale, full};
return update_equal(actual, terms);
};
for (std::size_t i = 0U; i < ar.Size(); ++i) {
const auto full = chart.p_dofs[i];
SCOPED_TRACE("increment=" + std::to_string(increment) + ", full=" + std::to_string(full));
EXPECT_TRUE(update_equal(trial.root.p[i],
{predictor.coordinates[i], p.beta * h * h * ar[i]}));
EXPECT_TRUE(update_equal(trial.vp[i],
{predictor.velocity[i], p.gamma * h * ar[i]}));
trace_update(0U, full, trial.root.p[i] - private_p[i],
{h * private_v[i], h * h * (0.5 - p.beta) * private_ar[i],
p.beta * h * h * ar[i]});
const double velocity_term = h * private_v[i];
const double old_rate_term = h * h * (0.5 - p.beta) * private_ar[i];
const double new_rate_term = p.beta * h * h * ar[i];
const auto whole_pair = reduced::CheckPairIncrement(trial.root.p[i],
candidate_p_low[i], private_p[i],
{velocity_term, old_rate_term, new_rate_term},
std::abs(velocity_term) + std::abs(old_rate_term) + std::abs(new_rate_term));
EXPECT_TRUE(whole_pair.storage_pass);
EXPECT_TRUE(whole_pair.arithmetic_pass) << whole_pair.ratio;
const double stored_increment = requested_sum[i] + requested_compensation[i];
const double actual_increment = trial.root.p[i] - private_p[i];
const double trace_defect = std::abs(actual_increment - stored_increment);
const double trace_scale = requested_absolute_sum[i];
const auto requested_pair = reduced::CheckPairIncrement(trial.root.p[i],
candidate_p_low[i], private_p[i],
{requested_sum[i], requested_compensation[i]}, trace_scale);
const double trace_ratio = trace_scale == 0.0
? (trace_defect == 0.0 ? 0.0 : std::numeric_limits<double>::infinity())
: trace_defect / trace_scale;
if (!(std::isfinite(trace_ratio) && trace_ratio <= 1.0e-12))
std::cout << std::setprecision(17) << "C08_POSITION_REPRESENTATION_DIAGNOSTIC h=" << h
<< " increment=" << increment << " full=" << full
<< " old_p=" << private_p[i] << " start_p=" << chosen_start_p[i]
<< " p=" << trial.root.p[i] << " actual=" << actual_increment
<< " expected=" << stored_increment << " signed_sum=" << requested_sum[i]
<< " compensation=" << requested_compensation[i] << " scale=" << trace_scale
<< " ratio=" << trace_ratio << " last_before=" << last_p_before_update[i]
<< " last_delta=" << last_requested_delta[i]
<< " addition_low_sum=" << addition_low_sum[i]
<< " candidate_low=" << candidate_p_low[i]
<< " pair_ratio=" << requested_pair.ratio
<< " pair_storage=" << requested_pair.storage_pass
<< " subtraction_low=" << AdditionLowPart(trial.root.p[i], -private_p[i], actual_increment)
<< "\n";
EXPECT_TRUE(requested_pair.storage_pass);
EXPECT_TRUE(requested_pair.arithmetic_pass) << requested_pair.ratio;
if (trace_ratio >= update_trace[1U].ratio)
update_trace[1U] = {trace_ratio, actual_increment, stored_increment, trace_scale, full,
requested_sum[i], requested_compensation[i]};
// This subtraction is diagnostic only; N3 requires the absolute
// velocity update and the resolved position increment.
trace_update(2U, full, trial.vp[i] - private_v[i],
{h * (1.0 - p.gamma) * private_ar[i], p.gamma * h * ar[i]});
EXPECT_TRUE(update_equal((1.0 + p.alpha) * trial.ap[i],
{ar[i], p.alpha * private_a[i]}));
}
if (increment <= 5U) {
for (std::size_t check = 0U; check < update_trace.size(); ++check) {
const auto& value = update_trace[check];
std::cout << std::setprecision(17) << "C08_RESOLVED_TRACE h=" << h
<< " increment=" << increment << " check=" << check
<< " full=" << value.full << " ratio=" << value.ratio
<< " actual=" << value.actual << " expected=" << value.expected
<< " scale=" << value.scale << " signed_sum=" << value.signed_sum
<< " compensation=" << value.compensation << "\n";
}
}
for (std::size_t i = 0U; i < zero.Size(); ++i)
EXPECT_DOUBLE_EQ(frame.algorithmic_acceleration[i],
(1.0 + p.alpha) * frame.acceleration[i] - p.alpha * old.acceleration[i]);
private_p = trial.root.p;
private_v = trial.vp;
private_a = trial.ap;
private_ar = ar;
private_p_radius = candidate_p_radius;
private_v_radius = vp_radius;
private_a_radius = ap_radius;
private_ar_radius = ar_radius;
seed = trial.root.z;
arithmetic_radii = full_indicator;
// Private carried-Ar remains a sparse p-coordinate history. Its full
// diagnostic lift is already represented by the frozen graph result;
// do not overwrite a plane support with a canonical label index.
converged = true;
break;
}
if (iteration == 100U) break;
const double cv = p.gamma / (p.beta * h);
const double ca = 1.0 / ((1.0 + p.alpha) * p.beta * h * h);
auto system = Require(reduced::AssembleAugmentedReducedSystem(
assembly, chart, operators, cv, ca, fixture.length, h));
const bool angular_only = reduced::ShouldRefineAngular(
chart, frame.free_residual_metrics, translation_corrections,
translation_private_algorithmic_pass);
auto selected = Require(reduced::SelectAugmentedSolve(system, chart,
angular_only));
fesa::Vector selected_solution(selected.rhs.Size());
Require(tangent.FactorizeFresh(selected.tangent));
Require(tangent.SolveCorrection(selected.rhs, selected_solution));
Require(fesa::hht_internal::CheckLinearSolve(selected.tangent,
selected.rhs,
selected_solution));
auto solution = Require(reduced::EmbedAugmentedSolution(
selected, selected_solution, system.rhs.Size()));
const auto correction = Require(reduced::MapAugmentedCorrection(
chart, system, solution, cv, ca));
correction_pass = true;
std::array<double, 4U> next_translation_corrections{};
for (const auto full : fixture.dofs.FreeDofs()) {
const double scale = full % 6U < 3U ? fixture.length : 1.0;
const std::array<double, 4U> values{
correction.coordinate[full], h * correction.velocity[full],
h * h * correction.acceleration[full],
h * h * (1.0 + p.alpha) * correction.acceleration[full]};
for (std::size_t rate = 0U; rate < values.size(); ++rate) {
const double ratio = std::abs(values[rate]) / scale;
correction_pass = correction_pass && ratio <= 1.0e-10;
if (full % 6U < 3U) {
if (!std::isfinite(ratio))
next_translation_corrections[rate] =
std::numeric_limits<double>::infinity();
else
next_translation_corrections[rate] = std::max(
next_translation_corrections[rate], ratio);
}
}
}
for (std::size_t i = 0U; i < ar.Size(); ++i) {
const double delta = correction.p[i] / (p.beta * h * h);
const double scale = reduced::PhysicalCoordinateChannel(chart, i) == 0U ? fixture.length : 1.0;
const double algorithmic_ratio = std::abs(h * h * delta) / scale;
correction_pass = correction_pass && algorithmic_ratio <= 1.0e-10;
// Preserve the physical Newton increment even when pp + beta*h*h*Ar
// cancels large terms. Ar retains its own equivalent HHT recurrence.
if (selected.angular_only && reduced::PhysicalCoordinateChannel(chart, i) == 0U)
continue;
record_requested_increment(i, correction.p[i]);
last_p_before_update[i] = candidate_p[i];
last_requested_delta[i] = correction.p[i];
const double raw_sum = candidate_p[i] + correction.p[i];
addition_low_sum[i] += AdditionLowPart(candidate_p[i], correction.p[i], raw_sum);
AdvanceCoordinate(correction.p[i], &candidate_p[i], &candidate_p_low[i],
&candidate_p_pair_radius[i]);
candidate_p_radius[i] = UpSum(candidate_p_pair_radius[i],
std::abs(candidate_p_low[i]));
ar[i] += delta;
ar_radius[i] = AddRadius(ar[i], ar_radius[i], QuotientRadius(
correction.p[i], 0.0, p.beta * h * h, HhtCoordinateFactorRadius(p, h)));
}
translation_corrections = next_translation_corrections;
// Retain the original private k*delta_ap=delta_Ar identity. The
// sparse Lp projection below is an additional basis-lift evidence, not
// a replacement for this recurrence check.
const double private_acceleration_rate =
1.0 / ((1.0 + p.alpha) * p.beta * h * h);
fesa::Vector delta_algorithmic_acceleration = correction.acceleration;
delta_algorithmic_acceleration.Scale(1.0 + p.alpha);
const auto projected_delta_algorithmic = Require(reduced::ProjectPhysical(
chart, delta_algorithmic_acceleration));
for (std::size_t i = 0U; i < ar.Size(); ++i) {
const double delta_ar = correction.p[i] / (p.beta * h * h);
const double private_delta_a = private_acceleration_rate * correction.p[i];
EXPECT_TRUE(private_algorithmic_equal((1.0 + p.alpha) * private_delta_a,
delta_ar));
double lifted = 0.0;
double lifted_scale = std::abs(delta_ar);
for (const auto& entry : chart.p_columns[i].support) {
const double term =
entry.value * delta_algorithmic_acceleration[entry.dof];
lifted += term;
lifted_scale += std::abs(term);
}
const double lifted_defect = std::abs(lifted - delta_ar);
const bool lifted_pass = std::isfinite(lifted_defect) &&
std::isfinite(lifted_scale) &&
(lifted_scale == 0.0 ? lifted_defect == 0.0
: lifted_defect / lifted_scale <= 1.0e-12);
EXPECT_TRUE(lifted_pass);
// This computed projection is diagnostic only: it includes rounded
// plane/algebraic leakage and must not replace private FB-08 history.
EXPECT_TRUE(std::isfinite(projected_delta_algorithmic[i]));
if (reduced::PhysicalCoordinateChannel(chart, i) != 0U) continue;
const double ratio = std::abs(h * h * delta_ar) / fixture.length;
translation_private_algorithmic_pass =
translation_private_algorithmic_pass && std::isfinite(ratio) &&
ratio <= 1.0e-10 && private_algorithmic_equal(
(1.0 + p.alpha) * private_delta_a, delta_ar);
}
copy_translation_rates = selected.angular_only;
if (copy_translation_rates) {
retained_velocity = trial.vp;
retained_acceleration = trial.ap;
retained_velocity_radius = vp_radius;
retained_acceleration_radius = ap_radius;
}
seed = trial.root.z;
for (std::size_t i = 0U; i < seed.Size(); ++i)
seed[i] += correction.z[i];
last.emplace(LastSolve{std::move(system), std::move(selected),
std::move(selected_solution), std::move(solution)});
}
if (!converged)
throw std::runtime_error{"C08 reduced Newton exhausted 100 corrections at h=" +
std::to_string(h) + ", increment=" + std::to_string(increment)};
const auto physical_oracle = Integrate(fixture, frame);
EXPECT_LE(std::abs(frame.kinetic_energy -
static_cast<double>(physical_oracle.kinetic)) / energy0, 1.0e-10);
EXPECT_LE(static_cast<double>(Norm(Add(physical_oracle.momentum,
Scale(L3{frame.linear_momentum[0], frame.linear_momentum[1],
frame.linear_momentum[2]}, -1.0L)))) /
static_cast<double>(initial_oracle.momentum_scale), 1.0e-10);
EXPECT_LE(static_cast<double>(Norm(Add(physical_oracle.angular_momentum,
Scale(L3{frame.angular_momentum[0], frame.angular_momentum[1],
frame.angular_momentum[2]}, -1.0L)))) /
static_cast<double>(initial_oracle.angular_momentum_scale), 1.0e-10);
const double energy = frame.kinetic_energy + frame.physical_strain_energy +
frame.drilling_energy;
if (!manufactured)
evidence.energy_error =
std::max(evidence.energy_error, std::abs(energy - energy0) / energy0);
double dp = 0.0;
double dl = 0.0;
for (std::size_t c = 0; c < 3; ++c) {
dp = std::hypot(dp, frame.linear_momentum[c] - initial_momentum[c]);
dl = std::hypot(dl,
frame.angular_momentum[c] - initial_angular_momentum[c]);
}
if (!manufactured) {
evidence.momentum_error =
std::max(evidence.momentum_error,
dp / static_cast<double>(initial_oracle.momentum_scale));
evidence.angular_momentum_error = std::max(
evidence.angular_momentum_error,
dl / static_cast<double>(initial_oracle.angular_momentum_scale));
}
for (std::size_t node = 0; node < fixture.domain->Nodes().size(); ++node)
evidence.maximum_rotation = std::max(
evidence.maximum_rotation,
static_cast<double>(
Norm(Triple(frame.generalized_coordinates, 6U * node + 3U))));
for (std::size_t node = 0; node < fixture.domain->Nodes().size(); ++node)
for (std::size_t channel = 0; channel < 2U; ++channel) {
const auto offset = 6U * node + 3U * channel;
const double scale = channel == 0U ? fixture.length : 1.0;
evidence.algorithmic_acceleration_gap = std::max(
evidence.algorithmic_acceleration_gap,
evidence.end_time * evidence.end_time / scale *
static_cast<double>(Norm(
Add(Triple(frame.algorithmic_acceleration, offset),
Scale(Triple(frame.acceleration, offset), -1.0L)))));
}
for (std::size_t i = 0; i < frame.hidden_constraint.Size(); ++i) {
if (frame.hidden_constraint_scale[i] == 0.0)
EXPECT_DOUBLE_EQ(frame.hidden_constraint[i], 0.0);
else
EXPECT_LE(std::abs(frame.hidden_constraint[i]) /
frame.hidden_constraint_scale[i],
1.0e-10);
const double vscale = frame.hidden_velocity_defect_scale[i];
const double ascale = frame.hidden_acceleration_defect_scale[i];
if (vscale > 0.0)
evidence.hidden_velocity =
std::max(evidence.hidden_velocity,
std::abs(frame.hidden_velocity_defect[i]) / vscale);
else
EXPECT_DOUBLE_EQ(frame.hidden_velocity_defect[i], 0.0);
if (ascale > 0.0)
evidence.hidden_acceleration =
std::max(evidence.hidden_acceleration,
std::abs(frame.hidden_acceleration_defect[i]) / ascale);
else
EXPECT_DOUBLE_EQ(frame.hidden_acceleration_defect[i], 0.0);
}
evidence.history.push_back(frame);
}
std::cout << std::setprecision(17) << (manufactured ? "C08a" : "C08b")
<< " h=" << h;
if (!manufactured)
std::cout << " energy=" << evidence.energy_error
<< " P=" << evidence.momentum_error
<< " L=" << evidence.angular_momentum_error;
std::cout << " phi_max=" << evidence.maximum_rotation
<< " a_A_gap=" << evidence.algorithmic_acceleration_gap
<< " hidden_v=" << evidence.hidden_velocity
<< " hidden_a=" << evidence.hidden_acceleration << " solve_noise=";
for (const double value : evidence.solve_noise) std::cout << value << ',';
std::cout << " arithmetic_scope=frozen-operator-first-order-indicator round_noise=";
for (const double value : evidence.arithmetic_noise)
std::cout << value << ',';
std::cout << " selector_history_radii=";
for (const double value : evidence.selector_history_arithmetic_noise)
std::cout << value << ',';
std::cout << " public_A_indicator=";
for (const double value : evidence.full_filter_arithmetic_noise)
std::cout << value << ',';
std::cout << '\n';
return evidence;
}
std::array<double, 6> Differences(const GridEvidence& coarse,
const GridEvidence& fine,
const double length) {
std::array<double, 6> error{};
for (std::size_t frame = 0; frame < coarse.history.size(); ++frame) {
const auto& a = coarse.history[frame];
const auto& b = fine.history[static_cast<std::size_t>(
std::llround(coarse.h / fine.h)) * frame];
const std::array<const fesa::Vector*, 3> first{
&a.generalized_coordinates, &a.velocity, &a.acceleration};
const std::array<const fesa::Vector*, 3> second{
&b.generalized_coordinates, &b.velocity, &b.acceleration};
for (std::size_t node = 0; node < a.nodal_internal_order.size(); ++node) {
for (std::size_t channel = 0; channel < 2; ++channel) {
const auto offset = 6U * node + 3U * channel;
for (std::size_t rate = 0; rate < 3; ++rate) {
const double normalization =
(rate == 0 ? 1.0
: rate == 1 ? coarse.end_time
: coarse.end_time * coarse.end_time) /
(channel == 0U ? length : 1.0);
const double difference = static_cast<double>(
Norm(Add(Triple(*first[rate], offset),
Scale(Triple(*second[rate], offset), -1.0L))));
error[2U * rate + channel] =
std::max(error[2U * rate + channel], normalization * difference);
}
}
}
}
return error;
}
std::array<double, 6> FrameError(const Frame& actual, const Frame& exact,
const double length, const double end_time) {
std::array<double, 6> error{};
const std::array<const fesa::Vector*, 3> first{
&actual.generalized_coordinates, &actual.velocity, &actual.acceleration};
const std::array<const fesa::Vector*, 3> second{
&exact.generalized_coordinates, &exact.velocity, &exact.acceleration};
for (std::size_t node = 0; node < actual.generalized_coordinates.Size() / 6U;
++node) {
for (std::size_t channel = 0; channel < 2U; ++channel) {
const auto offset = 6U * node + 3U * channel;
for (std::size_t rate = 0; rate < 3U; ++rate) {
const double factor = (rate == 0U ? 1.0
: rate == 1U ? end_time
: end_time * end_time) /
(channel == 0U ? length : 1.0);
error[2U * rate + channel] = std::max(
error[2U * rate + channel],
factor * static_cast<double>(Norm(
Add(Triple(*first[rate], offset),
Scale(Triple(*second[rate], offset), -1.0L)))));
}
}
}
return error;
}
std::array<double, 6> RecoveredFrameError(
const Frame& actual, const fesa::Vector& recovered_acceleration,
const Frame& exact, const double length, const double end_time) {
auto recovered = actual;
recovered.acceleration = recovered_acceleration;
return FrameError(recovered, exact, length, end_time);
}
std::array<double, 6> RecoveredDifferences(const GridEvidence& coarse,
const GridEvidence& fine,
const double length) {
std::array<double, 6> error{};
for (std::size_t frame = 0; frame < coarse.history.size(); ++frame) {
const auto& a = coarse.history[frame];
const auto& b = fine.history[static_cast<std::size_t>(
std::llround(coarse.h / fine.h)) * frame];
const std::array<const fesa::Vector*, 3> first{
&a.generalized_coordinates, &a.velocity,
&coarse.recovered_acceleration[frame].value};
const std::array<const fesa::Vector*, 3> second{
&b.generalized_coordinates, &b.velocity,
&fine.recovered_acceleration[static_cast<std::size_t>(
std::llround(coarse.h / fine.h)) * frame].value};
for (std::size_t node = 0; node < a.nodal_internal_order.size(); ++node) {
for (std::size_t channel = 0; channel < 2; ++channel) {
const auto offset = 6U * node + 3U * channel;
const double scale = channel == 0U ? length : 1.0;
for (std::size_t rate = 0; rate < 3; ++rate) {
const double normalization =
(rate == 0U ? 1.0
: rate == 1U ? coarse.end_time
: coarse.end_time * coarse.end_time) /
scale;
error[2U * rate + channel] = std::max(
error[2U * rate + channel],
normalization *
static_cast<double>(
Norm(Add(Triple(*first[rate], offset),
Scale(Triple(*second[rate], offset), -1.0L)))));
}
}
}
}
return error;
}
std::array<double, 2> AccelerationSplit(const Frame& basis,
const fesa::Vector& first,
const fesa::Vector& second,
const double end_time) {
const auto dofs = basis.generalized_coordinates.Size();
if (dofs % 6U != 0U || basis.velocity.Size() != dofs ||
basis.acceleration.Size() != dofs || first.Size() != dofs ||
second.Size() != dofs) {
throw std::runtime_error{
"C08 acceleration split requires matching nodal vectors."};
}
std::array<double, 2> split{};
for (std::size_t node = 0; node < dofs / 6U; ++node) {
const auto offset = 6U * node + 3U;
auto vector3 = [](const L3& value) {
return fesa::Vector3{static_cast<double>(value[0]),
static_cast<double>(value[1]),
static_cast<double>(value[2])};
};
const auto rotation = Require(fesa::EvaluateDynamicRotation(
vector3(Triple(basis.generalized_coordinates, offset)), {0.0, 0.0, 1.0},
vector3(Triple(basis.velocity, offset)),
vector3(Triple(basis.acceleration, offset))));
const L3 null{rotation.drill_null[0], rotation.drill_null[1],
rotation.drill_null[2]};
const L3 difference =
Add(Triple(first, offset), Scale(Triple(second, offset), -1.0L));
split[0] = std::max(
split[0], end_time * end_time *
static_cast<double>(std::abs(Dot(null, difference))));
split[1] = std::max(split[1],
end_time * end_time *
static_cast<double>(Norm(Cross(null, difference))));
}
return split;
}
TEST(HhtShellPhysics, ReducedManufacturedControllerKeepsExactInitialOrbit) {
const auto fixture = MakeFixture(1U);
const auto grid = RunPlate(fixture, 1.0 / 64.0, true);
ASSERT_EQ(grid.history.size(), 65U);
const auto exact = RigidOrbit(fixture, 0.0);
for (std::size_t i = 0U; i < exact.generalized_coordinates.Size(); ++i) {
EXPECT_DOUBLE_EQ(grid.history.front().generalized_coordinates[i], exact.generalized_coordinates[i]);
EXPECT_DOUBLE_EQ(grid.history.front().velocity[i], exact.velocity[i]);
EXPECT_DOUBLE_EQ(grid.history.front().acceleration[i], exact.acceleration[i]);
EXPECT_DOUBLE_EQ(grid.history.front().algorithmic_acceleration[i], exact.acceleration[i]);
}
for (std::size_t index = 0U; index < grid.history.size(); ++index) {
const auto& frame = grid.history[index];
EXPECT_DOUBLE_EQ(frame.time, static_cast<double>(index) / 64.0);
EXPECT_EQ(frame.time_integration_formulation,
fesa::DynamicTimeIntegrationFormulation::kReducedCoordinateHht);
EXPECT_EQ(frame.generalized_coordinates.Size(), fixture.dofs.FullDofCount());
EXPECT_EQ(frame.velocity.Size(), fixture.dofs.FullDofCount());
EXPECT_EQ(frame.acceleration.Size(), fixture.dofs.FullDofCount());
for (const auto* channel : {&frame.generalized_coordinates, &frame.velocity,
&frame.acceleration, &frame.algorithmic_acceleration})
for (std::size_t i = 0U; i < channel->Size(); ++i)
EXPECT_TRUE(std::isfinite((*channel)[i]));
for (const double residual : frame.free_residual_metrics)
EXPECT_LE(residual, 1.0e-10);
for (const auto* defect : {&frame.hidden_constraint,
&frame.hidden_velocity_defect,
&frame.hidden_acceleration_defect})
for (std::size_t i = 0U; i < defect->Size(); ++i)
EXPECT_TRUE(std::isfinite((*defect)[i]));
}
EXPECT_EQ(grid.history.back().time_integration_formulation,
fesa::DynamicTimeIntegrationFormulation::kReducedCoordinateHht);
EXPECT_DOUBLE_EQ(grid.history.back().time, 1.0);
EXPECT_GT(grid.maximum_rotation, 0.5);
}
TEST(HhtShellPhysics, C08aRigidManufacturedOrbitReducedCoordinateTimeConvergence) {
const auto fixture = MakeFixture(1U);
std::vector<GridEvidence> grids;
std::vector<std::array<double, 6>> errors;
for (const unsigned denominator : {16U, 32U, 64U, 128U, 256U, 512U, 1024U}) {
SCOPED_TRACE(denominator);
grids.push_back(
RunPlate(fixture, 1.0 / static_cast<double>(denominator), true));
const auto& grid = grids.back();
EXPECT_GT(grid.maximum_rotation, 0.5);
std::array<double, 6> maximum{};
std::array<double, 6> first{};
std::array<double, 6> late{};
std::array<double, 6> final{};
std::array<double, 6> maximum_time{};
std::array<double, 6> recovered_maximum{};
std::array<double, 6> recovered_maximum_time{};
std::array<double, 2> recovered_acceleration_split{};
double recovery_pre_hidden2 = 0.0;
double recovery_post_hidden2 = 0.0;
double recovery_mass_delta_a = 0.0;
double angular_acceleration_null_error = 0.0;
double angular_acceleration_physical_error = 0.0;
for (std::size_t index = 0; index < grid.history.size(); ++index) {
const auto& frame = grid.history[index];
ASSERT_EQ(frame.hidden_velocity_defect.Size(), fixture.domain->Nodes().size());
ASSERT_EQ(frame.hidden_velocity_defect_scale.Size(), frame.hidden_velocity_defect.Size());
ASSERT_EQ(frame.hidden_acceleration_defect.Size(), frame.hidden_velocity_defect.Size());
ASSERT_EQ(frame.hidden_acceleration_defect_scale.Size(), frame.hidden_velocity_defect.Size());
for (std::size_t node = 0U; node < frame.hidden_velocity_defect.Size(); ++node) {
for (const auto values : {
std::make_pair(frame.hidden_velocity_defect[node], frame.hidden_velocity_defect_scale[node]),
std::make_pair(frame.hidden_acceleration_defect[node], frame.hidden_acceleration_defect_scale[node])}) {
ASSERT_TRUE(std::isfinite(values.first));
ASSERT_TRUE(std::isfinite(values.second));
ASSERT_GE(values.second, 0.0);
if (values.second == 0.0)
EXPECT_DOUBLE_EQ(values.first, 0.0);
else
EXPECT_LE(std::abs(values.first) / values.second, 1.0e-10);
}
}
const auto exact = RigidOrbit(fixture, frame.time);
const auto error =
FrameError(frame, exact, fixture.length, grid.end_time);
const auto recovered_error =
RecoveredFrameError(frame, grid.recovered_acceleration[index].value,
exact, fixture.length, grid.end_time);
const auto split =
AccelerationSplit(exact, grid.recovered_acceleration[index].value,
exact.acceleration, grid.end_time);
for (std::size_t channel = 0; channel < 6U; ++channel) {
if (recovered_error[channel] > recovered_maximum[channel])
recovered_maximum_time[channel] = frame.time;
recovered_maximum[channel] =
std::max(recovered_maximum[channel], recovered_error[channel]);
}
for (std::size_t part = 0; part < 2U; ++part)
recovered_acceleration_split[part] =
std::max(recovered_acceleration_split[part], split[part]);
recovery_pre_hidden2 =
std::max(recovery_pre_hidden2,
grid.recovered_acceleration[index].pre_hidden2_relative);
recovery_post_hidden2 =
std::max(recovery_post_hidden2,
grid.recovered_acceleration[index].post_hidden2_relative);
recovery_mass_delta_a =
std::max(recovery_mass_delta_a,
grid.recovered_acceleration[index].mass_delta_a_relative);
if (error[5] > maximum[5]) {
double largest = 0.0;
for (std::size_t node = 0; node < fixture.domain->Nodes().size();
++node) {
const auto offset = 6U * node + 3U;
const auto difference =
Add(Triple(frame.acceleration, offset),
Scale(Triple(exact.acceleration, offset), -1.0L));
const double magnitude = static_cast<double>(Norm(difference));
if (magnitude <= largest) continue;
largest = magnitude;
const auto phi = Triple(frame.generalized_coordinates, offset);
const auto velocity = Triple(frame.velocity, offset);
const auto acceleration = Triple(frame.acceleration, offset);
auto vector3 = [](const L3& value) {
return fesa::Vector3{static_cast<double>(value[0]),
static_cast<double>(value[1]),
static_cast<double>(value[2])};
};
const auto rotation = Require(fesa::EvaluateDynamicRotation(
vector3(phi), {0.0, 0.0, 1.0}, vector3(velocity),
vector3(acceleration)));
const L3 null{rotation.drill_null[0], rotation.drill_null[1],
rotation.drill_null[2]};
angular_acceleration_null_error =
static_cast<double>(std::abs(Dot(null, difference)));
angular_acceleration_physical_error =
static_cast<double>(Norm(Cross(null, difference)));
}
}
for (std::size_t channel = 0; channel < 6U; ++channel) {
if (error[channel] > maximum[channel])
maximum_time[channel] = frame.time;
maximum[channel] = std::max(maximum[channel], error[channel]);
if (index == 1U) first[channel] = error[channel];
if (frame.time >= grid.end_time / 4.0)
late[channel] = std::max(late[channel], error[channel]);
}
final = error;
}
errors.push_back(maximum);
std::cout << "C08a temporal_error h=" << grid.h;
for (const auto& row :
{std::make_pair(" first=", first), std::make_pair(" late=", late),
std::make_pair(" maximum=", maximum),
std::make_pair(" final=", final),
std::make_pair(" maximum_time=", maximum_time)}) {
std::cout << row.first;
for (const double value : row.second) std::cout << value << ',';
}
std::cout << " max_angular_a_null=" << angular_acceleration_null_error
<< " max_angular_a_physical="
<< angular_acceleration_physical_error << '\n';
std::cout << "C08a recovered_diagnostic h=" << grid.h << " max_time_error=";
for (const double value : recovered_maximum) std::cout << value << ',';
std::cout << " max_time=";
for (const double value : recovered_maximum_time) std::cout << value << ',';
std::cout << " angular_a_null=" << recovered_acceleration_split[0]
<< " angular_a_physical=" << recovered_acceleration_split[1]
<< " pre_hidden2=" << recovery_pre_hidden2
<< " post_hidden2=" << recovery_post_hidden2
<< " Mdeltaa=" << recovery_mass_delta_a << '\n';
}
for (std::size_t channel = 0; channel < 6U; ++channel) {
SCOPED_TRACE(channel);
EXPECT_LE(errors.back()[channel], 1.0e-4);
for (std::size_t grid = grids.size() - 3U; grid + 1U < grids.size(); ++grid) {
if (errors[grid][channel] == 0.0 && errors[grid + 1U][channel] == 0.0)
continue;
ASSERT_GT(errors[grid][channel], 0.0);
ASSERT_GT(errors[grid + 1U][channel], 0.0);
EXPECT_GE(std::log2(errors[grid][channel] / errors[grid + 1U][channel]),
1.8);
}
for (std::size_t grid = grids.size() - 3U; grid < grids.size(); ++grid)
EXPECT_GT(errors[grid][channel],
100.0 * std::max(grids[grid].solve_noise[channel],
grids[grid].arithmetic_noise[channel]))
<< "N7 inconclusive: temporal error does not resolve "
"solve/arithmetic noise.";
}
}
TEST(HhtShellPhysics, C08bInitialPhysicalSpectrumProbe) {
const auto fixture = MakeFixture(2U);
const auto parameters = Require(fesa::MakeHhtParameters(-0.05));
const auto assembly =
Evaluate(fixture, EmptyFrame(fixture), 1.0 / 64.0, parameters, false);
const auto print_matrix = [](const char* prefix,
const fesa::SparseMatrix& matrix) {
ASSERT_EQ(matrix.Rows(), 54U);
ASSERT_EQ(matrix.Columns(), 54U);
ASSERT_EQ(matrix.RowOffsets().size(), 55U);
for (std::size_t row = 0; row < matrix.Rows(); ++row) {
std::array<double, 54U> values{};
for (std::size_t index = matrix.RowOffsets()[row];
index < matrix.RowOffsets()[row + 1U]; ++index) {
const auto column = matrix.ColumnIndices()[index];
ASSERT_LT(column, values.size());
values[column] = matrix.Values()[index];
}
std::cout << prefix << " row=" << row
<< " values=" << std::setprecision(17);
for (const double value : values) std::cout << value << ',';
std::cout << '\n';
}
};
// Diagnostic matrix dump for offline static condensation/eigensolution only.
print_matrix("C08b_initial_M", assembly.mass);
print_matrix("C08b_initial_K", assembly.internal_tangent);
}
TEST(HhtShellPhysics,
PredictorStartCompletesPreviouslyFailingLumpedFreePlateGrid) {
const auto fixture = MakeFixture(2U);
const auto evidence = RunPlate(fixture, 1.0 / 256.0, false);
ASSERT_EQ(evidence.history.size(), 65U);
EXPECT_DOUBLE_EQ(evidence.history.back().time, kEndTime);
EXPECT_GE(evidence.maximum_rotation, 0.2);
EXPECT_LT(evidence.maximum_rotation, std::acos(-1.0));
}
TEST(HhtShellPhysics,
C08bFreeFiniteRotationPlateSelfConvergenceAndPhysicalInvariants) {
const auto fixture = MakeFixture(2U);
std::vector<GridEvidence> grids;
for (const unsigned denominator :
{64U, 128U, 256U, 512U, 1024U, 32768U, 65536U, 131072U}) {
SCOPED_TRACE(denominator);
grids.push_back(
RunPlate(fixture, 1.0 / static_cast<double>(denominator), false));
EXPECT_GE(grids.back().maximum_rotation, 0.2);
}
std::vector<std::array<double, 6>> differences;
for (std::size_t pair = 0; pair + 1U < grids.size(); ++pair) {
differences.push_back(
Differences(grids[pair], grids[pair + 1U], fixture.length));
std::cout << "C08b self_difference h=" << grids[pair].h << ' ';
for (const double value : differences.back()) std::cout << value << ',';
std::cout << '\n';
const auto recovered =
RecoveredDifferences(grids[pair], grids[pair + 1U], fixture.length);
std::array<double, 2> acceleration_split{};
double recovery_pre_hidden2 = 0.0;
double recovery_post_hidden2 = 0.0;
double recovery_mass_delta_a = 0.0;
for (std::size_t frame = 0; frame < grids[pair].history.size(); ++frame) {
const auto fine_frame = static_cast<std::size_t>(
std::llround(grids[pair].h / grids[pair + 1U].h)) * frame;
const auto split = AccelerationSplit(
grids[pair + 1U].history[fine_frame],
grids[pair].recovered_acceleration[frame].value,
grids[pair + 1U].recovered_acceleration[fine_frame].value,
grids[pair].end_time);
for (std::size_t part = 0; part < 2U; ++part)
acceleration_split[part] =
std::max(acceleration_split[part], split[part]);
}
for (const auto* grid : {&grids[pair], &grids[pair + 1U]}) {
for (const auto& recovery : grid->recovered_acceleration) {
recovery_pre_hidden2 =
std::max(recovery_pre_hidden2, recovery.pre_hidden2_relative);
recovery_post_hidden2 =
std::max(recovery_post_hidden2, recovery.post_hidden2_relative);
recovery_mass_delta_a =
std::max(recovery_mass_delta_a, recovery.mass_delta_a_relative);
}
}
std::cout << "C08b recovered_diagnostic h=" << grids[pair].h
<< " common_time_difference=";
for (const double value : recovered) std::cout << value << ',';
std::cout << " angular_a_null=" << acceleration_split[0]
<< " angular_a_physical=" << acceleration_split[1]
<< " pre_hidden2=" << recovery_pre_hidden2
<< " post_hidden2=" << recovery_post_hidden2
<< " Mdeltaa=" << recovery_mass_delta_a << '\n';
}
for (std::size_t channel = 0; channel < 6; ++channel) {
SCOPED_TRACE(channel);
EXPECT_LE(differences.back()[channel], 1.0e-4);
for (std::size_t pair = differences.size() - 3U;
pair + 1U < differences.size(); ++pair) {
const double first = differences[pair][channel];
const double second = differences[pair + 1U][channel];
if (first == 0.0 && second == 0.0) continue;
ASSERT_GT(first, 0.0);
ASSERT_GT(second, 0.0);
// For the preserved 1024 -> 32768 gap use the unequal-grid
// Richardson ratio; the final 2:1 triple reduces to log2(d0/d1).
const double coarse_ratio = grids[pair].h / grids[pair + 1U].h;
const double fine_ratio = grids[pair + 1U].h / grids[pair + 2U].h;
const double threshold = (std::pow(coarse_ratio, 1.8) - 1.0) /
(1.0 - std::pow(fine_ratio, -1.8));
EXPECT_GE(first / second, threshold);
}
for (std::size_t pair = 1U; pair < differences.size(); ++pair) {
const double noise = std::max(grids[pair].solve_noise[channel],
grids[pair].arithmetic_noise[channel]) +
std::max(grids[pair + 1U].solve_noise[channel],
grids[pair + 1U].arithmetic_noise[channel]);
EXPECT_GT(differences[pair][channel], 100.0 * noise)
<< "N7 inconclusive: temporal difference does not resolve "
"solve/arithmetic noise.";
}
}
for (const auto member :
{&GridEvidence::energy_error, &GridEvidence::angular_momentum_error}) {
const auto final = grids.size() - 1U;
EXPECT_LT(grids[final - 1U].*member, grids[final - 2U].*member);
EXPECT_LT(grids[final].*member, grids[final - 1U].*member);
EXPECT_LE(grids[final].*member, 0.6 * (grids[final - 1U].*member));
EXPECT_LE(grids[final].*member, 1.0e-3);
}
// In this centered, all-free fixture the exact HHT branch conserves total
// linear momentum at every h. Endpoint equilibrium and CheckUpdates above
// exercise its balance/update identities; temporal strict decrease is not a
// defined P accuracy measure when the exact error is zero.
EXPECT_LE(grids.back().momentum_error, 1.0e-3);
}
TEST(HhtReducedIncrement, FrozenNearestCoordinatesPassPairArithmeticWithoutChangingHigh) {
const auto first = reduced::CheckPairIncrement(
.071707935756794142, -2.6003877354211524e-18, .071709031890658609,
{-1.0961338644697638e-6, -2.0834558726391122e-22},
1.0961338644697645e-6);
EXPECT_TRUE(first.storage_pass);
EXPECT_TRUE(first.arithmetic_pass);
EXPECT_EQ(first.defect, 0.0);
const auto worst = reduced::CheckPairIncrement(
.07170874594859544, -3.911862569787138e-18, .07170906968934286,
{-3.23740747422167e-7, -5.266112996573883e-23},
3.237407474221672e-7);
EXPECT_TRUE(worst.storage_pass);
EXPECT_TRUE(worst.arithmetic_pass);
EXPECT_EQ(worst.defect, 0.0);
// The independently logged requested trace is not reconstructed from high.
const auto missing_low = reduced::CheckPairIncrement(
.07170874594859544, 0.0, .07170906968934286,
{-3.23740747422167e-7, -5.266112996573883e-23},
3.237407474221672e-7);
EXPECT_FALSE(missing_low.arithmetic_pass);
}
TEST(HhtReducedIncrement, StorageRequiresNearestEvenIncludingBothMidpointDirections) {
const double half_upper_spacing = std::ldexp(1.0, -53);
const double odd = std::nextafter(1.0, 2.0);
EXPECT_TRUE(reduced::CheckPairIncrement(1.0, half_upper_spacing, 1.0,
{half_upper_spacing}, half_upper_spacing).storage_pass);
EXPECT_TRUE(reduced::CheckPairIncrement(1.0, -std::ldexp(1.0, -54), 1.0,
{-std::ldexp(1.0, -54)}, std::ldexp(1.0, -54)).storage_pass);
EXPECT_TRUE(reduced::CheckPairIncrement(-1.0, -half_upper_spacing, -1.0,
{-half_upper_spacing}, half_upper_spacing).storage_pass);
const auto odd_tie = reduced::CheckPairIncrement(odd, -half_upper_spacing,
1.0, {half_upper_spacing}, half_upper_spacing);
EXPECT_TRUE(odd_tie.arithmetic_pass);
EXPECT_FALSE(odd_tie.storage_pass);
EXPECT_FALSE(reduced::CheckPairIncrement(-odd, half_upper_spacing, -1.0,
{-half_upper_spacing}, half_upper_spacing).storage_pass);
const auto misplaced_high = reduced::CheckPairIncrement(1.0,
std::ldexp(1.0, -52), 1.0, {std::ldexp(1.0, -52)}, std::ldexp(1.0, -52));
EXPECT_TRUE(misplaced_high.arithmetic_pass);
EXPECT_FALSE(misplaced_high.storage_pass);
}
TEST(HhtReducedIncrement, WholeUpdateAndIndependentTraceRejectMissingCorrection) {
const double correction = std::ldexp(1.0, -54);
const auto valid_whole = reduced::CheckPairIncrement(1.0, 2.0 * correction,
1.0, {correction, 0.0, correction}, 2.0 * correction);
const auto valid_trace = reduced::CheckPairIncrement(1.0, 2.0 * correction,
1.0, {2.0 * correction, 0.0}, 2.0 * correction);
EXPECT_TRUE(valid_whole.arithmetic_pass && valid_whole.storage_pass);
EXPECT_TRUE(valid_trace.arithmetic_pass && valid_trace.storage_pass);
EXPECT_FALSE(reduced::CheckPairIncrement(1.0, correction, 1.0,
{correction, 0.0, correction}, 2.0 * correction).arithmetic_pass);
EXPECT_FALSE(reduced::CheckPairIncrement(1.0, correction, 1.0,
{2.0 * correction, 0.0}, 2.0 * correction).arithmetic_pass);
EXPECT_FALSE(reduced::CheckPairIncrement(1.0, -2.0 * correction, 1.0,
{2.0 * correction, 0.0}, 2.0 * correction).arithmetic_pass);
}
TEST(HhtReducedIncrement, ExactThresholdRejectsTailHiddenByRoundedRatio) {
const double threshold = 1.0e-12;
const double tail = std::ldexp(1.0, -100);
const auto equality = reduced::CheckPairIncrement(0.0, 0.0, 0.0,
{threshold}, 1.0);
EXPECT_TRUE(equality.arithmetic_pass);
const auto above = reduced::CheckPairIncrement(0.0, 0.0, 0.0,
{threshold, tail}, 1.0);
EXPECT_EQ(above.ratio, threshold);
EXPECT_FALSE(above.arithmetic_pass);
const auto above_decimal = reduced::CheckPairIncrement(0.0, 0.0, 0.0,
{threshold, std::ldexp(1.0, -94)}, 1.0);
EXPECT_EQ(above_decimal.ratio, threshold);
EXPECT_FALSE(above_decimal.arithmetic_pass);
const auto below = reduced::CheckPairIncrement(0.0, 0.0, 0.0,
{threshold, -tail}, 1.0);
EXPECT_TRUE(below.arithmetic_pass);
const double largest = std::numeric_limits<double>::max();
const auto maximum = reduced::CheckPairIncrement(0.0, 0.0, 0.0,
{1.0}, largest);
EXPECT_TRUE(maximum.arithmetic_pass);
const double tiny = std::numeric_limits<double>::denorm_min();
const auto unsupported = reduced::CheckPairIncrement(0.0, 0.0, 0.0,
{tiny}, std::ldexp(1.0, -1000));
EXPECT_FALSE(unsupported.arithmetic_pass);
}
TEST(HhtReducedIncrement, ZeroScaleAndSubnormalArithmeticRemainExact) {
const double tiny = std::numeric_limits<double>::denorm_min();
const auto zero = reduced::CheckPairIncrement(1.0, 0.0, 1.0, {0.0}, 0.0);
EXPECT_TRUE(zero.arithmetic_pass && zero.storage_pass);
const auto fixed_zero = reduced::CheckPairIncrement(0.0, 0.0, 0.0, {0.0}, 0.0);
EXPECT_TRUE(fixed_zero.arithmetic_pass && fixed_zero.storage_pass);
const auto tiny_valid = reduced::CheckPairIncrement(tiny, 0.0, 0.0, {tiny}, tiny);
EXPECT_TRUE(tiny_valid.arithmetic_pass && tiny_valid.storage_pass);
EXPECT_EQ(tiny_valid.defect, 0.0);
const auto tiny_nonzero = reduced::CheckPairIncrement(1.0, tiny, 1.0, {0.0}, 0.0);
EXPECT_TRUE(tiny_nonzero.storage_pass);
EXPECT_FALSE(tiny_nonzero.arithmetic_pass);
EXPECT_EQ(tiny_nonzero.defect, tiny);
EXPECT_FALSE(reduced::CheckPairIncrement(0.0, tiny, 0.0, {tiny}, tiny).storage_pass);
}
TEST(HhtReducedIncrement, NonfiniteAndOverflowFailBeforeAcceptance) {
const double nan = std::numeric_limits<double>::quiet_NaN();
const double infinity = std::numeric_limits<double>::infinity();
for (const auto invalid : {nan, infinity}) {
EXPECT_FALSE(reduced::CheckPairIncrement(invalid, 0.0, 0.0, {0.0}, 0.0).arithmetic_pass);
EXPECT_FALSE(reduced::CheckPairIncrement(0.0, invalid, 0.0, {0.0}, 0.0).storage_pass);
EXPECT_FALSE(reduced::CheckPairIncrement(0.0, 0.0, invalid, {0.0}, 0.0).arithmetic_pass);
EXPECT_FALSE(reduced::CheckPairIncrement(0.0, 0.0, 0.0, {invalid}, 1.0).arithmetic_pass);
EXPECT_FALSE(reduced::CheckPairIncrement(0.0, 0.0, 0.0, {0.0}, invalid).arithmetic_pass);
}
const double largest = std::numeric_limits<double>::max();
const auto overflow = reduced::CheckPairIncrement(largest, largest, 0.0,
{largest}, largest);
EXPECT_FALSE(overflow.arithmetic_pass);
EXPECT_FALSE(overflow.storage_pass);
#if defined(_M_X64) || defined(__SSE2__)
const auto saved_mxcsr = _mm_getcsr();
for (const auto mask : {0x8000U, 0x0040U}) {
_mm_setcsr(saved_mxcsr | mask);
const auto unsupported = reduced::CheckPairIncrement(1.0,
std::numeric_limits<double>::denorm_min(), 1.0, {0.0}, 0.0);
_mm_setcsr(saved_mxcsr);
EXPECT_FALSE(unsupported.arithmetic_pass);
EXPECT_FALSE(unsupported.storage_pass);
}
#endif
}
TEST(HhtReducedLifecycle, AllowsEvaluationAfterCorrection100ButNeverCorrection101) {
using fesa::hht_reduced_internal::CanApplyOuterCorrection;
using fesa::hht_reduced_internal::kMaximumOuterCorrections;
EXPECT_TRUE(CanApplyOuterCorrection(0U));
EXPECT_TRUE(CanApplyOuterCorrection(99U));
EXPECT_FALSE(CanApplyOuterCorrection(100U));
EXPECT_FALSE(CanApplyOuterCorrection(101U));
// The actual executor evaluates acceptance before this correction guard.
// This verifies its budget policy, not a physical 100-correction trajectory.
for (const bool accept_at_100 : {false, true}) {
std::uint32_t corrections = 0U;
std::uint32_t last_evaluation = 0U;
bool accepted = false;
for (std::uint32_t evaluation = 0U;
evaluation <= kMaximumOuterCorrections; ++evaluation) {
last_evaluation = evaluation;
if (accept_at_100 && evaluation == 100U) {
accepted = true;
break;
}
if (!CanApplyOuterCorrection(evaluation)) break;
++corrections;
}
EXPECT_EQ(corrections, 100U);
EXPECT_EQ(last_evaluation, 100U);
EXPECT_EQ(accepted, accept_at_100);
}
}
std::string ReadLifecycleLog(const std::filesystem::path& path) {
std::ifstream stream(path);
std::ostringstream text;
text << stream.rdbuf();
return text.str();
}
class FailAfterAcceptedIncrement final : public fesa::TangentSolver {
public:
explicit FailAfterAcceptedIncrement(std::filesystem::path path)
: path_(std::move(path)) {}
fesa::Status FactorizeFresh(const fesa::SparseMatrix& matrix) override {
std::istringstream lines(ReadLifecycleLog(path_));
std::string line;
while (std::getline(lines, line)) {
if (line.find("\tincrement=1\t") != std::string::npos &&
line.find("\toutcome=accepted") != std::string::npos) {
++failures;
return fesa::Status::Failure(fesa::FailureCategory::kSolver,
{{fesa::Severity::kError, "c10-injected-backend", {}, "DYNAMIC", {},
"Injected backend failure after accepted increment one."}});
}
}
return solver_.FactorizeFresh(matrix);
}
fesa::Status SolveCorrection(const fesa::Vector& rhs,
fesa::Vector& correction) const override {
return solver_.SolveCorrection(rhs, correction);
}
std::size_t failures = 0U;
private:
std::filesystem::path path_;
fesa::MklPardisoTangentSolver solver_;
};
TEST(HhtReducedLifecycle, FailedExecutionDoesNotPublishCandidateOrContaminateRetry) {
auto fixture = MakeFixture(1U, true);
const fesa::SerialParallelFor parallel;
const auto run = [&](fesa::TangentSolver& tangent,
const std::filesystem::path& path) {
fesa::MklPardisoSolver positive;
fesa::NewtonIterationLogWriter log;
Require(log.Open(path));
auto result = fesa::ExecuteReducedHht(*fixture.domain, log, parallel,
positive, tangent);
Require(log.Close());
return result;
};
const auto directory = std::filesystem::temp_directory_path();
const auto baseline_path = directory / "fesa-c10-baseline.log";
const auto failure_path = directory / "fesa-c10-failure.log";
const auto retry_path = directory / "fesa-c10-retry.log";
fesa::MklPardisoTangentSolver baseline_solver;
auto baseline = run(baseline_solver, baseline_path);
ASSERT_TRUE(baseline.HasValue()) << Message(baseline.GetStatus());
ASSERT_EQ(baseline.Value().state.AcceptedFrames().size(), 3U);
EXPECT_GT(baseline.Value().state.AcceptedFrames()[1U].generalized_coordinates[0U],
0.0);
FailAfterAcceptedIncrement failing(failure_path);
const auto failed = run(failing, failure_path);
ASSERT_FALSE(failed.HasValue());
EXPECT_EQ(failing.failures, 1U);
EXPECT_EQ(failed.GetStatus().Diagnostics().front().code, "c10-injected-backend");
const auto partial_log = ReadLifecycleLog(failure_path);
EXPECT_NE(partial_log.find("\tincrement=2\t"), std::string::npos);
EXPECT_NE(partial_log.find("\toutcome=failed"), std::string::npos);
std::istringstream lines(partial_log);
std::string line;
while (std::getline(lines, line)) {
if (line.find("\toutcome=initial") != std::string::npos ||
line.find("\toutcome=accepted") != std::string::npos) {
EXPECT_NE(ReadLifecycleLog(baseline_path).find(line + '\n'), std::string::npos);
EXPECT_EQ(line.find("\tincrement=2\t"), std::string::npos);
}
}
fesa::MklPardisoTangentSolver retry_solver;
auto retry = run(retry_solver, retry_path);
ASSERT_TRUE(retry.HasValue()) << Message(retry.GetStatus());
const auto& expected = baseline.Value().state.AcceptedFrames();
const auto& actual = retry.Value().state.AcceptedFrames();
ASSERT_EQ(actual.size(), expected.size());
for (std::size_t frame = 0U; frame < actual.size(); ++frame) {
EXPECT_EQ(actual[frame].frame_index, expected[frame].frame_index);
EXPECT_EQ(actual[frame].iteration_count, expected[frame].iteration_count);
EXPECT_EQ(actual[frame].time, expected[frame].time);
for (const auto member : {&Frame::generalized_coordinates, &Frame::velocity,
&Frame::acceleration, &Frame::algorithmic_acceleration,
&Frame::internal_force, &Frame::inertia_force, &Frame::residual,
&Frame::reaction, &Frame::hidden_constraint,
&Frame::hidden_velocity_defect, &Frame::hidden_acceleration_defect}) {
const auto& left = actual[frame].*member;
const auto& right = expected[frame].*member;
ASSERT_EQ(left.Size(), right.Size());
for (std::size_t i = 0U; i < left.Size(); ++i)
EXPECT_EQ(left[i], right[i]);
}
}
EXPECT_EQ(ReadLifecycleLog(retry_path), ReadLifecycleLog(baseline_path));
for (const auto& path : {baseline_path, failure_path, retry_path})
std::filesystem::remove(path);
// Result<DynamicExecutionResult> contains no value after failure. Private
// histories are local to that execution; they are not directly observable.
}
} // namespace