1465 lines
65 KiB
C++
1465 lines
65 KiB
C++
#include <gtest/gtest.h>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <iomanip>
|
|
#include <iostream>
|
|
#include <limits>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "analysis/hht_dynamic_internal.h"
|
|
#include "fesa/analysis/analysis_model.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) {
|
|
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;
|
|
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 5^3 rectangle quadrature and derivative-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];
|
|
L3 position{};
|
|
L3 velocity{};
|
|
L3 acceleration{};
|
|
for (std::size_t node = 0; node < 4; ++node) {
|
|
position =
|
|
Add(position,
|
|
Scale(Add(x[node], Scale(director[node], z)), shape[node]));
|
|
velocity =
|
|
Add(velocity, Scale(Add(v[node], Scale(director_rate[node], z)),
|
|
shape[node]));
|
|
acceleration =
|
|
Add(acceleration,
|
|
Scale(Add(a[node], Scale(director_acceleration[node], z)),
|
|
shape[node]));
|
|
}
|
|
oracle.kinetic += 0.5L * weight * Dot(velocity, velocity);
|
|
oracle.power += weight * Dot(velocity, acceleration);
|
|
oracle.power_scale += weight * Norm(velocity) * Norm(acceleration);
|
|
oracle.momentum = Add(oracle.momentum, Scale(velocity, weight));
|
|
oracle.angular_momentum =
|
|
Add(oracle.angular_momentum,
|
|
Scale(Cross(position, velocity), weight));
|
|
oracle.momentum_rate =
|
|
Add(oracle.momentum_rate, Scale(acceleration, weight));
|
|
oracle.angular_momentum_rate =
|
|
Add(oracle.angular_momentum_rate,
|
|
Scale(Cross(position, acceleration), weight));
|
|
oracle.momentum_scale += weight * Norm(velocity);
|
|
oracle.angular_momentum_scale +=
|
|
weight * Norm(Cross(position, velocity));
|
|
oracle.momentum_rate_scale += weight * Norm(acceleration);
|
|
oracle.angular_momentum_rate_scale +=
|
|
weight * Norm(Cross(position, acceleration));
|
|
for (std::size_t node = 0; node < 4; ++node) {
|
|
const auto offset = 6U * shell.node_indices[node];
|
|
for (std::size_t c = 0; c < 3; ++c) {
|
|
force[offset + c] += weight * shape[node] * acceleration[c];
|
|
force[offset + 3U + c] += weight * shape[node] * 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, integral(N_i X)=X_i/3 and
|
|
// integral(N_i z^2)=t^3/12. Differentiate those independent volume 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 / 3.0L), &rates[0], 6U * node);
|
|
Store(Scale(x[2], 0.2L / 3.0L), &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(-rotation.null_directional_second[c] * frame->external_force[row],
|
|
&second, &second_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,
|
|
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{};
|
|
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;
|
|
};
|
|
|
|
fesa::Vector ScaledResidual(const Fixture& fixture, const Frame& frame) {
|
|
fesa::Vector rhs(fixture.dofs.FreeDofCount());
|
|
for (std::size_t index = 0; index < rhs.Size(); ++index) {
|
|
const auto full = fixture.dofs.FreeDofs()[index];
|
|
rhs[index] =
|
|
-frame.residual[full] * (full % 6U < 3U ? fixture.length : 1.0);
|
|
}
|
|
return rhs;
|
|
}
|
|
|
|
struct LastSolve {
|
|
fesa::SparseMatrix tangent;
|
|
fesa::Vector rhs;
|
|
fesa::Vector correction;
|
|
};
|
|
|
|
void RecordNoise(const Fixture& fixture, const Assembly& accepted,
|
|
const fesa::HhtPredictor& predictor, const LastSolve& last,
|
|
const fesa::HhtParameters& p,
|
|
fesa::MklPardisoTangentSolver* solver,
|
|
GridEvidence* evidence) {
|
|
auto residual = last.tangent.Multiply(last.correction);
|
|
residual.Axpy(-1.0, last.rhs);
|
|
residual.Scale(-1.0);
|
|
fesa::Vector linear_defect(residual.Size());
|
|
Require(solver->FactorizeFresh(last.tangent));
|
|
Require(solver->SolveCorrection(residual, linear_defect));
|
|
Require(fesa::hht_internal::CheckLinearSolve(last.tangent, residual,
|
|
linear_defect));
|
|
const auto endpoint = Require(fesa::hht_internal::ScaledFree(
|
|
accepted.effective_tangent, fixture.dofs, fixture.length));
|
|
const auto rhs = ScaledResidual(fixture, accepted.frame);
|
|
fesa::Vector nonlinear_defect(rhs.Size());
|
|
Require(solver->FactorizeFresh(endpoint));
|
|
Require(solver->SolveCorrection(rhs, nonlinear_defect));
|
|
Require(
|
|
fesa::hht_internal::CheckLinearSolve(endpoint, rhs, nonlinear_defect));
|
|
const double u = std::ldexp(1.0, -53);
|
|
const double gamma32 = 32.0 * u / (1.0 - 32.0 * u);
|
|
const std::array<double, 3> propagation{
|
|
1.0, evidence->end_time * p.gamma / (p.beta * evidence->h),
|
|
evidence->end_time * evidence->end_time /
|
|
((1.0 + p.alpha) * p.beta * evidence->h * evidence->h)};
|
|
for (std::size_t node = 0; node < fixture.domain->Nodes().size(); ++node) {
|
|
for (std::size_t channel = 0; channel < 2; ++channel) {
|
|
const auto offset = 6U * node + 3U * channel;
|
|
const double scale = channel == 0U ? fixture.length : 1.0;
|
|
const double eta =
|
|
static_cast<double>(Norm(Triple(last.correction, offset)) +
|
|
Norm(Triple(linear_defect, offset)) +
|
|
Norm(Triple(nonlinear_defect, offset)));
|
|
const double arithmetic =
|
|
gamma32 *
|
|
static_cast<double>(
|
|
Norm(Triple(accepted.frame.generalized_coordinates, offset)) +
|
|
Norm(Triple(predictor.coordinates, offset))) /
|
|
scale;
|
|
for (std::size_t rate = 0; rate < 3; ++rate) {
|
|
evidence->solve_noise[2U * rate + channel] =
|
|
std::max(evidence->solve_noise[2U * rate + channel],
|
|
propagation[rate] * eta);
|
|
evidence->arithmetic_noise[2U * rate + channel] =
|
|
std::max(evidence->arithmetic_noise[2U * rate + channel],
|
|
propagation[rate] * arithmetic);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
assembly = Evaluate(fixture, frame, h, p, true);
|
|
frame = assembly.frame;
|
|
if (manufactured)
|
|
CompleteManufacturedConstraints(fixture, assembly, null_nodes, &positive,
|
|
&tangent, &frame);
|
|
else
|
|
Require(fesa::hht_internal::CompleteConstraintEvidence(
|
|
*fixture.domain, fixture.dofs, assembly, fixture.length, null_nodes,
|
|
positive, tangent, &frame));
|
|
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});
|
|
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;
|
|
const auto predictor =
|
|
Require(fesa::PredictHht(old.generalized_coordinates, old.velocity,
|
|
old.algorithmic_acceleration, h, p));
|
|
frame.generalized_coordinates = old.generalized_coordinates;
|
|
frame.is_initial = false;
|
|
frame.frame_index = increment;
|
|
frame.increment_index = increment;
|
|
frame.time = static_cast<double>(increment) * h;
|
|
if (manufactured)
|
|
frame.external_force =
|
|
Integrate(fixture, RigidOrbit(fixture, frame.time)).force;
|
|
bool correction_pass = true;
|
|
std::optional<LastSolve> last;
|
|
bool converged = false;
|
|
for (std::uint32_t iteration = 0U; iteration <= 100U; ++iteration) {
|
|
auto rates = Require(fesa::EvaluateHhtRates(
|
|
frame.generalized_coordinates, predictor, old.acceleration, h, p));
|
|
frame.velocity = std::move(rates.velocity);
|
|
frame.acceleration = std::move(rates.acceleration);
|
|
frame.algorithmic_acceleration =
|
|
std::move(rates.algorithmic_acceleration);
|
|
assembly = Evaluate(fixture, frame, h, p, false);
|
|
frame = assembly.frame;
|
|
if (correction_pass && frame.free_residual_metrics[0] <= 1.0e-10 &&
|
|
frame.free_residual_metrics[1] <= 1.0e-10) {
|
|
assembly = Evaluate(fixture, frame, h, p, true);
|
|
frame = assembly.frame;
|
|
if (manufactured)
|
|
CompleteManufacturedConstraints(fixture, assembly, null_nodes,
|
|
&positive, &tangent, &frame);
|
|
else
|
|
Require(fesa::hht_internal::CompleteConstraintEvidence(
|
|
*fixture.domain, fixture.dofs, assembly, fixture.length,
|
|
null_nodes, positive, tangent, &frame));
|
|
Require(fesa::hht_internal::CompleteEquilibrium(
|
|
*fixture.domain, fixture.dofs, assembly, &frame));
|
|
if (!last)
|
|
throw std::runtime_error{
|
|
"C08 nontrivial step lacks N7 last-solve evidence."};
|
|
RecordNoise(fixture, assembly, predictor, *last, p, &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;
|
|
converged = true;
|
|
break;
|
|
}
|
|
if (iteration == 100U) break;
|
|
auto matrix = Require(fesa::hht_internal::ScaledFree(
|
|
assembly.effective_tangent, fixture.dofs, fixture.length));
|
|
auto rhs = ScaledResidual(fixture, frame);
|
|
fesa::Vector correction(rhs.Size());
|
|
Require(tangent.FactorizeFresh(matrix));
|
|
Require(tangent.SolveCorrection(rhs, correction));
|
|
Require(fesa::hht_internal::CheckLinearSolve(matrix, rhs, correction));
|
|
std::array<double, 2> correction_norm{};
|
|
for (std::size_t i = 0; i < correction.Size(); ++i) {
|
|
const auto full = fixture.dofs.FreeDofs()[i];
|
|
frame.generalized_coordinates[full] +=
|
|
correction[i] * (full % 6U < 3U ? fixture.length : 1.0);
|
|
const auto channel = full % 6U < 3U ? 0U : 1U;
|
|
correction_norm[channel] =
|
|
std::max(correction_norm[channel], std::abs(correction[i]));
|
|
}
|
|
correction_pass = true;
|
|
for (const double norm : correction_norm)
|
|
for (const double factor :
|
|
{1.0, p.gamma / p.beta, 1.0 / ((1.0 + p.alpha) * p.beta),
|
|
1.0 / p.beta})
|
|
correction_pass = correction_pass && factor * norm <= 1.0e-10;
|
|
last.emplace(
|
|
LastSolve{std::move(matrix), std::move(rhs), std::move(correction)});
|
|
}
|
|
if (!converged)
|
|
throw std::runtime_error{
|
|
"C08 exhausted 100 Newton corrections at h=" + std::to_string(h) +
|
|
", increment=" + std::to_string(increment)};
|
|
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 << " round_noise=";
|
|
for (const double value : evidence.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[2U * 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[2U * 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[2U * 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, C08aRigidManufacturedOrbitFullCoordinateTimeConvergence) {
|
|
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}) {
|
|
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];
|
|
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 = 2U; grid < 4U; ++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 = 2U; grid < 5U; ++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.";
|
|
}
|
|
for (const auto member :
|
|
{&GridEvidence::hidden_velocity, &GridEvidence::hidden_acceleration}) {
|
|
EXPECT_LT(grids[3].*member, grids[2].*member);
|
|
EXPECT_LT(grids[4].*member, grids[3].*member);
|
|
}
|
|
}
|
|
|
|
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,
|
|
C08bFreeFiniteRotationPlateSelfConvergenceAndPhysicalInvariants) {
|
|
const auto fixture = MakeFixture(2U);
|
|
std::vector<GridEvidence> grids;
|
|
for (const unsigned denominator : {64U, 128U, 256U, 512U, 1024U}) {
|
|
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 = 2U * 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 = 1U; pair < 3U; ++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);
|
|
EXPECT_GE(std::log2(first / second), 1.8);
|
|
}
|
|
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::momentum_error,
|
|
&GridEvidence::angular_momentum_error}) {
|
|
EXPECT_LT(grids[3].*member, grids[2].*member);
|
|
EXPECT_LT(grids[4].*member, grids[3].*member);
|
|
EXPECT_LE(grids[4].*member, 0.6 * (grids[3].*member));
|
|
EXPECT_LE(grids[4].*member, 1.0e-3);
|
|
}
|
|
}
|
|
|
|
} // namespace
|