Files

1142 lines
51 KiB
C++

#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <functional>
#include <iomanip>
#include <iostream>
#include <limits>
#include <sstream>
#include <stdexcept>
#include <utility>
#include <vector>
#include "analysis/thermal_transient_algebra.h"
#include "fesa/analysis/transient_heat_procedure.h"
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
#include "thermal_gauss_oracle.h"
#include "thermal_slab_oracle.h"
#include "fesa/assembly/thermal_assembler.h"
namespace {
namespace ti = fesa::thermal_transient_internal;
using fesa::Vector;
using fesa::Vector3;
// Independent test arithmetic: outward endpoints after each scalar operation.
// No production interval, kernel, transcendental, or tolerance helper is used
// to construct the analytical expectations.
double Down(double x) {
return std::nextafter(x, -std::numeric_limits<double>::infinity());
}
double Up(double x) {
return std::nextafter(x, std::numeric_limits<double>::infinity());
}
struct I {
double lo, hi;
I(double x = 0) : lo(x), hi(x) {}
I(double lo, double hi) : lo(lo), hi(hi) {}
double Mid() const { return lo + (hi - lo) * .5; }
double Abs() const { return std::max(std::abs(lo), std::abs(hi)); }
};
I operator+(I a, I b) {
if (a.lo == 0 && a.hi == 0) return b;
if (b.lo == 0 && b.hi == 0) return a;
return {Down(a.lo + b.lo), Up(a.hi + b.hi)};
}
I operator-(I a, I b) { return a + I{-b.hi, -b.lo}; }
I operator*(I a, I b) {
if ((a.lo == 0 && a.hi == 0) || (b.lo == 0 && b.hi == 0)) return I{0};
const std::array<double, 4> v{a.lo * b.lo, a.lo * b.hi, a.hi * b.lo,
a.hi * b.hi};
return {Down(*std::min_element(v.begin(), v.end())),
Up(*std::max_element(v.begin(), v.end()))};
}
I operator/(I a, I b) {
if (b.lo <= 0 && b.hi >= 0)
throw std::runtime_error{"Oracle division includes zero."};
return a * I{Down(1 / b.hi), Up(1 / b.lo)};
}
I Sqrt(I a) {
if (a.lo < 0)
throw std::runtime_error{"Oracle square root includes negative values."};
if (a.hi == 0) return I{0};
return {a.lo == 0 ? 0 : Down(std::sqrt(a.lo)), Up(std::sqrt(a.hi))};
}
I Square(I x) {
if (x.lo == 0 && x.hi == 0) return I{0};
const double lower =
x.lo <= 0 && x.hi >= 0 ? 0 : Down(std::min(x.lo * x.lo, x.hi * x.hi));
return {lower, Up(x.Abs() * x.Abs())};
}
I Gamma(std::size_t n) {
return (I{static_cast<double>(n)} * I{0x1p-53}) /
(I{1} - I{static_cast<double>(n)} * I{0x1p-53});
}
I Pi() {
static const I value = [] {
// Machin's formula; alternating arctan remainder after terms 0..63.
const auto atan = [](double denominator) {
const I q = I{1} / I{denominator}, q2 = q * q;
I power = q, sum;
for (int j = 0; j < 64; ++j) {
sum = sum + power / I{static_cast<double>(2 * j + 1)};
power = power * q2 * I{-1};
}
const double tail = (power / I{129}).Abs();
return sum + I{-tail, tail};
};
return I{16} * atan(5) - I{4} * atan(239);
}();
return value;
}
I Cos(I x) {
if (x.hi < 0) x = I{-x.hi, -x.lo};
if (x.lo > Pi().hi / 2) {
const auto reduced = Cos(Pi() - x);
return I{-reduced.hi, -reduced.lo};
}
// |x|<=pi<4. Thirty-two terms; the omitted absolute term bounds the
// alternating tail after decreasing terms begin. All polynomial work is
// enclosed independently of a libm accuracy assumption.
I term{1}, sum{1};
const I x2 = x * x;
for (int j = 1; j < 32; ++j) {
term = term * x2 * I{-1} / I{static_cast<double>((2 * j - 1) * 2 * j)};
sum = sum + term;
}
const double tail = (term * x2 / I{63. * 64.}).Abs();
return sum + I{-tail, tail};
}
I ExpNegative(I x) {
if (x.lo < 0) throw std::runtime_error{"Negative exponential argument."};
int squares = 0;
while (x.hi > .5) {
x = x / I{2};
++squares;
}
I term{1}, sum{1};
for (int j = 1; j <= 64; ++j) {
term = term * x * I{-1} / I{static_cast<double>(j)};
sum = sum + term;
}
const double tail = (term * x / I{65}).Abs();
sum = sum + I{-tail, tail};
for (int j = 0; j < squares; ++j) sum = sum * sum;
return sum;
}
I Power(I x, std::uint32_t n) {
I result{1};
for (; n; n >>= 1) {
if (n & 1U) result = result * x;
if (n > 1) x = x * x;
}
return result;
}
template <class T>
T Take(fesa::Result<T> r) {
if (!r.HasValue()) {
std::ostringstream text;
for (const auto& d : r.GetStatus().Diagnostics())
text << d.code << ": " << d.message << '\n';
throw std::runtime_error{text.str()};
}
return std::move(r.Value());
}
void Checked(const fesa::Status& status) {
if (!status.IsOk()) {
std::ostringstream text;
for (const auto& d : status.Diagnostics())
text << d.code << ": " << d.message << '\n';
throw std::runtime_error{text.str()};
}
}
fesa::ModelDefinition Mesh(
std::size_t n, double period, std::uint32_t count,
const std::function<double(std::size_t, std::size_t)>& initial) {
fesa::ModelDefinition m;
for (std::size_t j = 0; j <= n; ++j)
for (std::size_t i = 0; i <= n; ++i) {
const auto index = static_cast<fesa::EntityIndex>(j * (n + 1) + i);
m.nodes.push_back(
{{"plate", static_cast<std::int64_t>(index + 1),
std::to_string(index + 1)},
{static_cast<double>(i) / n, static_cast<double>(j) / n, 0},
{}});
m.thermal_initial_conditions.push_back({index, initial(i, j), {}});
}
m.thermal_materials = {{{"plate", 1, "1"}, "unit", 1, 1, 1, {}}};
m.thermal_sections = {{{"plate", 1, "1"}, "unit", 1, 0, {}}};
for (std::size_t j = 0; j < n; ++j)
for (std::size_t i = 0; i < n; ++i) {
const auto node = static_cast<fesa::EntityIndex>(j * (n + 1) + i);
const auto label = static_cast<std::int64_t>(j * n + i + 1);
m.thermal_elements.push_back(
{{"plate", label, std::to_string(label)},
{{node, node + 1, static_cast<fesa::EntityIndex>(node + n + 2),
static_cast<fesa::EntityIndex>(node + n + 1)}},
0,
{}});
}
m.thermal_step =
fesa::ThermalStepDefinition{"physics", period / count, period, count};
return m;
}
struct Prepared {
fesa::Domain domain;
fesa::AnalysisModel model;
fesa::DofManager dofs;
fesa::ThermalAssembly assembly;
fesa::ThermalAnalysisState state;
Prepared(fesa::ModelDefinition definition, const fesa::ParallelFor& parallel)
: domain(Take(fesa::Domain::Create(std::move(definition)))),
model(Take(fesa::AnalysisModel::Create(domain))),
dofs(Take(fesa::DofManager::CreateThermal(model))),
assembly(Take(fesa::AssembleThermal(model, dofs, parallel))),
state(Take(fesa::ThermalAnalysisState::Create(model, dofs, assembly))) {
}
};
struct RunMetrics {
double beta = 0, full_beta = 0, capacity_budget = 0;
double energy_defect = 0, energy_bound = 0, dissipation_defect = 0,
dissipation_bound = 0;
std::uint64_t temperature_values = 0, flux_values = 0;
std::vector<ti::ThermalStepEvidence> steps;
double last_free_residual_l1 = 0, last_free_residual_inf = 0,
last_reaction_evaluation_error = 0;
};
RunMetrics Advance(Prepared& p, const fesa::ParallelFor& parallel,
double mass_lower, double initial_capacity_budget = 0) {
fesa::MklPardisoSolver solver;
ti::ThermalFactorization cache;
ti::ThermalSystem system{
p.assembly.Conductivity(), p.assembly.Capacity(),
p.assembly.HeatInput(), &p.assembly.ConductivityError(),
&p.assembly.CapacityError(), &p.assembly.HeatInputError()};
ti::ThermalPartition partition{p.dofs.FreeDofs(), p.dofs.ConstrainedDofs(),
p.dofs.PrescribedValues()};
RunMetrics metrics;
I budget{initial_capacity_budget};
const std::uint64_t count = p.state.TimeGrid().Count();
for (std::uint64_t step = 1; step <= count; ++step) {
const auto schedule =
Take(p.state.TimeGrid().Frame(static_cast<std::uint32_t>(step)));
const auto& old = p.state.CommittedState().temperature;
auto next = Take(ti::SolveThermalIncrement(system, partition, old,
schedule.delta, solver, cache));
const auto& e = next.evidence;
metrics.steps.push_back(e);
EXPECT_LE(e.beta_cert, e.beta_limit);
EXPECT_LE(e.full_beta_cert, e.beta_limit);
EXPECT_LE(e.energy_defect, e.energy_bound);
if (e.dissipation_checked)
EXPECT_LE(e.dissipation_defect, e.dissipation_bound);
metrics.beta = std::max(metrics.beta, e.beta_cert);
metrics.full_beta = std::max(metrics.full_beta, e.full_beta_cert);
metrics.energy_defect = std::max(metrics.energy_defect, e.energy_defect);
metrics.energy_bound = std::max(metrics.energy_bound, e.energy_bound);
metrics.dissipation_defect =
std::max(metrics.dissipation_defect, e.dissipation_defect);
metrics.dissipation_bound =
std::max(metrics.dissipation_bound, e.dissipation_bound);
I residual_square;
for (auto row : partition.free)
residual_square =
residual_square + Square(I{next.certified_residual[row]});
// Approved contraction for structured homogeneous error, using observed
// component residual enclosures including E_C/E_K, never beta_limit.
budget = budget +
I{schedule.delta} * Sqrt(residual_square) / Sqrt(I{mass_lower});
if (step == count) {
I free_l1, reaction_error;
for (auto row : partition.free) {
free_l1 = free_l1 + I{next.certified_residual[row]};
metrics.last_free_residual_inf = std::max(
metrics.last_free_residual_inf, next.certified_residual[row]);
}
for (std::size_t c = 0; c < partition.constrained.size(); ++c) {
const auto row = partition.constrained[c];
I residual = I{-system.q[row]};
for (auto entry = system.c.RowOffsets()[row];
entry < system.c.RowOffsets()[row + 1]; ++entry) {
const auto col = system.c.ColumnIndices()[entry];
const I capacity =
I{system.c.Values()[entry]} + I{-system.c_error->Values()[entry],
system.c_error->Values()[entry]};
const I conductivity =
I{system.k.Values()[entry]} + I{-system.k_error->Values()[entry],
system.k_error->Values()[entry]};
residual = residual +
capacity * ((I{next.temperature[col]} - I{old[col]}) /
I{schedule.delta}) +
conductivity * I{next.temperature[col]};
}
residual =
residual + I{-(*system.q_error)[row], (*system.q_error)[row]};
reaction_error =
reaction_error + I{(residual - I{next.reaction[c]}).Abs()};
}
metrics.last_free_residual_l1 = free_l1.hi;
metrics.last_reaction_evaluation_error = reaction_error.hi;
}
auto flux = Take(p.assembly.RecoverFlux(next.temperature, parallel));
auto frame = Take(p.state.MakeCompletedFrame(
static_cast<std::uint32_t>(step), std::move(next.temperature),
std::move(next.reaction), std::move(flux)));
Checked(p.state.CommitAcceptedFrame(frame));
}
metrics.capacity_budget = budget.hi;
EXPECT_TRUE(p.state.IsComplete());
EXPECT_EQ(p.state.AcceptedFrames().size(), count + 1);
EXPECT_FALSE(p.state.AcceptedFrames()[0].completed.has_value());
for (const auto& frame : p.state.AcceptedFrames()) {
metrics.temperature_values += frame.temperature.Size();
if (frame.completed)
metrics.flux_values += frame.completed->global_heat_flux.size() * 72U;
}
return metrics;
}
void Print(const char* name, std::size_t mesh, std::uint32_t count,
const RunMetrics& m) {
std::cout << std::setprecision(17) << "THERMAL_PHYSICS case=" << name
<< " n=" << mesh << " steps=" << count << " beta=" << m.beta
<< " physical_beta=" << m.full_beta
<< " capacity_budget=" << m.capacity_budget
<< " energy_defect=" << m.energy_defect
<< " energy_bound=" << m.energy_bound
<< " dissipation_defect=" << m.dissipation_defect
<< " dissipation_bound=" << m.dissipation_bound
<< " temperature_values=" << m.temperature_values
<< " flux_values=" << m.flux_values << '\n';
}
// Exact tensor mass independently assembled from the rational Q1 and P2
// element integrals. This checks actual 2-D nodal/station vectors.
I CapacityNorm(const Vector& values, std::size_t n) {
constexpr int q[4][4] = {
{4, 2, 1, 2}, {2, 4, 2, 1}, {1, 2, 4, 2}, {2, 1, 2, 4}};
constexpr int z[3][3] = {{4, 2, -1}, {2, 16, 2}, {-1, 2, 4}};
I sum;
for (std::size_t y = 0; y < n; ++y)
for (std::size_t x = 0; x < n; ++x) {
const std::size_t a = y * (n + 1) + x;
const std::array<std::size_t, 4> nodes{a, a + 1, a + n + 2, a + n + 1};
for (std::size_t i = 0; i < 4; ++i)
for (std::size_t j = 0; j < 4; ++j)
for (std::size_t segment = 0; segment < 2; ++segment)
for (std::size_t u = 0; u < 3; ++u)
for (std::size_t v = 0; v < 3; ++v)
sum = sum + I{values[5 * nodes[i] + 2 * segment + u]} *
I{values[5 * nodes[j] + 2 * segment + v]} *
(I{static_cast<double>(q[i][j] * z[u][v])} /
I{2160. * n * n});
}
return Sqrt(sum);
}
TEST(ThermalPhysics, InsulatedUniformAndZeroRetainAllFramesWithConservation) {
fesa::SerialParallelFor parallel;
for (double temperature : {0., 2., -2.}) {
Prepared p{Mesh(2, .25, 8, [&](auto, auto) { return temperature; }),
parallel};
const auto metrics = Advance(p, parallel, Down(1. / 8640));
Print("uniform", 2, 8, metrics);
const double bound = (I{metrics.capacity_budget} / Sqrt(I{1. / 8640})).hi;
for (const auto& frame : p.state.AcceptedFrames())
for (std::size_t i = 0; i < frame.temperature.Size(); ++i)
EXPECT_LE(std::abs(frame.temperature[i] - temperature), bound);
if (temperature == 0) {
EXPECT_EQ(metrics.capacity_budget, 0);
EXPECT_EQ(metrics.energy_defect, 0);
EXPECT_EQ(metrics.dissipation_defect, 0);
}
EXPECT_EQ(metrics.flux_values, 8U * 4U * 72U);
}
}
void VerifyDiscreteEigenmode(const Prepared& p, const std::array<I, 9>& mode,
I lambda) {
constexpr int mass[4][4] = {
{4, 2, 1, 2}, {2, 4, 2, 1}, {1, 2, 4, 2}, {2, 1, 2, 4}};
constexpr int stiffness[4][4] = {
{4, -1, -2, -1}, {-1, 4, -1, -2}, {-2, -1, 4, -1}, {-1, -2, -1, 4}};
constexpr int z_mass[3][3] = {{4, 2, -1}, {2, 16, 2}, {-1, 2, 4}};
constexpr int z_stiffness[3][3] = {{7, -8, 1}, {-8, 16, -8}, {1, -8, 7}};
std::array<I, 405> cv{}, kv{};
for (std::size_t y = 0; y < 8; ++y)
for (std::size_t x = 0; x < 8; ++x) {
const auto node = y * 9 + x;
const std::array<std::size_t, 4> nodes{node, node + 1, node + 10,
node + 9};
for (std::size_t i = 0; i < 4; ++i)
for (std::size_t j = 0; j < 4; ++j)
for (std::size_t segment = 0; segment < 2; ++segment)
for (std::size_t a = 0; a < 3; ++a)
for (std::size_t b = 0; b < 3; ++b) {
const auto row = 5 * nodes[i] + 2 * segment + a;
const I m = I{static_cast<double>(mass[i][j])} / I{36 * 64.};
const I s = I{static_cast<double>(stiffness[i][j])} / I{6};
const I c = I{static_cast<double>(z_mass[a][b])} / I{60};
const I k = I{2. * z_stiffness[a][b]} / I{3};
cv[row] = cv[row] + m * c * mode[nodes[j] % 9];
kv[row] = kv[row] + (s * c + m * k) * mode[nodes[j] % 9];
}
}
const auto& c = p.assembly.Capacity();
const auto& k = p.assembly.Conductivity();
I independent_norm, actual_norm;
double residual_bound = 0;
const auto intersects = [](I a, I b) {
EXPECT_LE(std::max(a.lo, b.lo), std::min(a.hi, b.hi));
};
for (std::size_t row = 0; row < 405; ++row) {
I actual_c, actual_k;
for (auto entry = c.RowOffsets()[row]; entry < c.RowOffsets()[row + 1];
++entry) {
const I v = mode[(c.ColumnIndices()[entry] / 5) % 9];
actual_c = actual_c + (I{c.Values()[entry]} +
I{-p.assembly.CapacityError().Values()[entry],
p.assembly.CapacityError().Values()[entry]}) *
v;
actual_k =
actual_k + (I{k.Values()[entry]} +
I{-p.assembly.ConductivityError().Values()[entry],
p.assembly.ConductivityError().Values()[entry]}) *
v;
}
intersects(actual_c, cv[row]);
intersects(actual_k, kv[row]);
const I residual = kv[row] - lambda * cv[row];
EXPECT_LE(residual.lo, 0);
EXPECT_GE(residual.hi, 0);
const I assembled_residual = actual_k - lambda * actual_c;
EXPECT_LE(assembled_residual.lo, 0);
EXPECT_GE(assembled_residual.hi, 0);
residual_bound = std::max(residual_bound, assembled_residual.Abs());
independent_norm = independent_norm + mode[(row / 5) % 9] * cv[row];
actual_norm = actual_norm + mode[(row / 5) % 9] * actual_c;
}
const I analytical_norm = (I{2} + mode[1]) / I{6};
intersects(independent_norm, analytical_norm);
intersects(actual_norm, analytical_norm);
const I normalized = actual_norm / analytical_norm;
EXPECT_LE(normalized.lo, 1);
EXPECT_GE(normalized.hi, 1);
std::cout << std::setprecision(17)
<< "THERMAL_EIGENMODE residual_bound=" << residual_bound
<< " C_norm_squared_lower=" << actual_norm.lo
<< " C_norm_squared_upper=" << actual_norm.hi
<< " normalized_lower=" << normalized.lo
<< " normalized_upper=" << normalized.hi << '\n';
}
TEST(ThermalPhysics, DiscreteEigenmodeHasFirstOrderTimeConvergence) {
fesa::SerialParallelFor parallel;
std::array<I, 9> cosines;
double initial_error = 0;
for (std::size_t i = 0; i < cosines.size(); ++i) {
cosines[i] = Cos(Pi() * I{static_cast<double>(i)} / I{8});
initial_error =
std::max(initial_error, (cosines[i] - I{cosines[i].Mid()}).Abs());
}
const I lambda = I{384} * (I{1} - cosines[1]) / (I{2} + cosines[1]);
const double period = (I{1} / lambda).Mid();
const I exact = ExpNegative(lambda * I{period});
double previous_error = 0;
for (std::uint32_t count : {16U, 32U, 64U, 128U, 256U}) {
Prepared p{Mesh(8, period, count,
[&](std::size_t i, auto) { return cosines[i].Mid(); }),
parallel};
if (count == 16) VerifyDiscreteEigenmode(p, cosines, lambda);
const auto metrics =
Advance(p, parallel, Down(1. / (2160 * 64)), initial_error);
const auto& actual = p.state.CommittedState().temperature;
Vector error{actual.Size()}, mode{actual.Size()}, difference{actual.Size()};
const I be = Power(I{1} / (I{1} + lambda * I{period / count}), count);
double oracle_width = 0;
for (std::size_t node = 0; node < 81; ++node)
for (std::size_t a = 0; a < 5; ++a) {
const auto i = node % 9;
const I v = cosines[i] * exact, expected = cosines[i] * be;
error[5 * node + a] = actual[5 * node + a] - v.Mid();
mode[5 * node + a] = cosines[i].Mid();
difference[5 * node + a] = actual[5 * node + a] - expected.Mid();
oracle_width =
std::max(oracle_width, (expected - I{expected.Mid()}).Abs());
}
const I mode_norm = CapacityNorm(mode, 8);
const I error_norm = CapacityNorm(error, 8) / mode_norm;
const I agreement = CapacityNorm(difference, 8);
const I total_budget = I{metrics.capacity_budget} + I{oracle_width};
EXPECT_LE(agreement.hi, total_budget.hi);
EXPECT_LE((total_budget / mode_norm).hi, error_norm.lo / 1000);
if (previous_error > 0) {
const double order = std::log2(previous_error / error_norm.Mid());
EXPECT_GE(order, .97);
EXPECT_LE(order, 1.02);
std::cout << std::setprecision(17) << "THERMAL_TIME N=" << count
<< " error=" << error_norm.Mid() << " order=" << order
<< " oracle_width=" << oracle_width << '\n';
}
previous_error = error_norm.Mid();
Print("time", 8, count, metrics);
}
}
TEST(ThermalPhysics, SignedOverlappingAndConstrainedPowerBalanceByComponent) {
fesa::SerialParallelFor parallel;
auto definition = Mesh(1, .5, 8, [](auto, auto) { return 0.; });
for (fesa::EntityIndex i = 0; i < 4; ++i) {
auto node = definition.nodes[i];
node.source_id.instance_name = "other";
node.coordinates[0] += 3;
definition.nodes.push_back(node);
definition.thermal_initial_conditions.push_back({i + 4, 0, {}});
}
definition.thermal_elements.push_back(
{{"other", 1, "1"}, {{4, 5, 7, 6}}, 0, {}});
definition.thermal_model_boundaries = {{0, 11, 0, {}}};
definition.thermal_step->loads = {{0, 11, 3, {}, 0},
{1, 15, -3, {}, 1},
{4, 11, 2, {}, 2},
{4, 11, 3, {}, 3},
{6, 15, -5, {}, 4}};
auto without_constrained = definition;
without_constrained.thermal_step->loads.erase(
without_constrained.thermal_step->loads.begin());
Prepared p{std::move(definition), parallel},
other{std::move(without_constrained), parallel};
EXPECT_EQ(p.assembly.HeatInput()[20], 5);
EXPECT_EQ(p.assembly.HeatInput()[34], -5);
const auto metrics = Advance(p, parallel, Down(1. / 2160));
const auto other_metrics = Advance(other, parallel, Down(1. / 2160));
Print("signed-component", 1, 8, metrics);
Print("without-constrained-power", 1, 8, other_metrics);
const std::array<I, 5> weights{I{1} / I{48}, I{1} / I{12}, I{1} / I{24},
I{1} / I{12}, I{1} / I{48}};
for (std::size_t step = 1; step < p.state.AcceptedFrames().size(); ++step) {
const auto& frame = p.state.AcceptedFrames()[step];
const auto& old = p.state.AcceptedFrames()[step - 1];
const auto& other_frame = other.state.AcceptedFrames()[step];
std::array<I, 2> energy;
for (std::size_t i = 0; i < 40; ++i) {
energy[i / 20] =
energy[i / 20] +
weights[i % 5] * (I{frame.temperature[i]} - I{old.temperature[i]});
EXPECT_DOUBLE_EQ(frame.temperature[i], other_frame.temperature[i]);
}
const double reaction = frame.completed->reaction_heat_rate[0];
const I first = energy[0] - I{frame.time_increment} * I{reaction};
// Independent exact unit-volume quadrature and component inventory.
EXPECT_LE(first.Abs(), metrics.steps[step - 1].energy_bound);
EXPECT_LE(energy[1].Abs(), metrics.steps[step - 1].energy_bound);
const double other_reaction = other_frame.completed->reaction_heat_rate[0];
const I reaction_shift = I{reaction} - I{other_reaction} + I{3};
const I subtraction =
Gamma(4) * (I{std::abs(reaction)} + I{std::abs(other_reaction)} + I{3});
EXPECT_LE(reaction_shift.Abs(), subtraction.hi);
}
}
fesa::SparseMatrix ZeroLike(const fesa::SparseMatrix& a) {
std::vector<fesa::CooContribution> coo;
for (std::size_t i = 0; i < a.Rows(); ++i)
for (std::size_t j = a.RowOffsets()[i]; j < a.RowOffsets()[i + 1]; ++j)
coo.push_back({i, a.ColumnIndices()[j], 0, 0, j});
return Take(fesa::SparseMatrix::FromCoo(a.Rows(), a.Columns(), std::move(coo),
{a.RowOffsets(), a.ColumnIndices()}));
}
// Independent dense inverse with a posteriori interval residual certificate.
// Used only for small fixture partitions, never as a production solver.
double CertifiedInverse(const fesa::SparseMatrix& matrix,
const std::vector<std::size_t>& free) {
const auto n = free.size();
std::vector<std::vector<double>> a(n, std::vector<double>(n)),
z(n, std::vector<double>(n));
for (std::size_t i = 0; i < n; ++i) {
z[i][i] = 1;
for (std::size_t k = matrix.RowOffsets()[free[i]];
k < matrix.RowOffsets()[free[i] + 1]; ++k)
for (std::size_t j = 0; j < n; ++j)
if (matrix.ColumnIndices()[k] == free[j]) a[i][j] = matrix.Values()[k];
}
const auto original = a;
for (std::size_t k = 0; k < n; ++k) {
std::size_t pivot = k;
for (std::size_t i = k + 1; i < n; ++i)
if (std::abs(a[i][k]) > std::abs(a[pivot][k])) pivot = i;
if (a[pivot][k] == 0)
throw std::runtime_error{"Singular independent inverse."};
std::swap(a[k], a[pivot]);
std::swap(z[k], z[pivot]);
const double scale = a[k][k];
for (std::size_t j = 0; j < n; ++j) {
a[k][j] /= scale;
z[k][j] /= scale;
}
for (std::size_t i = 0; i < n; ++i)
if (i != k) {
const double factor = a[i][k];
for (std::size_t j = 0; j < n; ++j) {
a[i][j] -= factor * a[k][j];
z[i][j] -= factor * z[k][j];
}
}
}
double theta = 0, znorm = 0;
for (std::size_t i = 0; i < n; ++i) {
I defect, row;
for (std::size_t j = 0; j < n; ++j) {
I entry{static_cast<double>(i == j)};
for (std::size_t k = 0; k < n; ++k)
entry = entry - I{z[i][k]} * I{original[k][j]};
defect = defect + I{entry.Abs()};
row = row + I{std::abs(z[i][j])};
}
theta = std::max(theta, defect.hi);
znorm = std::max(znorm, row.hi);
}
if (theta >= 1)
throw std::runtime_error{"Independent inverse certificate inconclusive."};
return (I{znorm} / (I{1} - I{theta})).hi;
}
TEST(ThermalPhysics,
PrivateFullEndStripRespectsThicknessMaterialJumpAndFoldResistance) {
fesa::SerialParallelFor parallel;
for (bool folded : {false, true}) {
auto m = Mesh(1, 1, 1, [](auto, auto) { return 0.; });
m.nodes.push_back(
{{"plate", 5, "5"}, {folded ? 1. : 2., 0, folded ? 1. : 0.}, {}});
m.nodes.push_back(
{{"plate", 6, "6"}, {folded ? 1. : 2., 1, folded ? 1. : 0.}, {}});
m.thermal_initial_conditions.push_back({4, 0, {}});
m.thermal_initial_conditions.push_back({5, 0, {}});
m.thermal_materials.push_back({{"plate", 2, "2"}, "high-k", 10, 1, 1, {}});
m.thermal_sections.push_back({{"plate", 2, "2"}, "thin", .5, 1, {}});
m.thermal_elements.push_back({{"plate", 2, "2"}, {{1, 4, 5, 3}}, 1, {}});
Prepared p{std::move(m), parallel};
const auto zero = ZeroLike(p.assembly.Capacity());
ti::ThermalPartition partition;
std::vector<double> g;
for (std::size_t node = 0; node < 6; ++node)
for (std::size_t station = 0; station < 5; ++station) {
if (node == 1 || node == 3)
partition.free.push_back(5 * node + station);
else {
partition.constrained.push_back(5 * node + station);
g.push_back(node >= 4 ? 1. : 0.);
}
}
partition.prescribed = Vector{g.size()};
for (std::size_t i = 0; i < g.size(); ++i) partition.prescribed[i] = g[i];
ti::ThermalFactorization cache;
fesa::MklPardisoSolver solver;
const auto result = Take(ti::SolveThermalIncrement(
{p.assembly.Conductivity(), zero, p.assembly.HeatInput(),
&p.assembly.ConductivityError(), &zero, &p.assembly.HeatInputError()},
partition, Vector{30}, 1, solver, cache));
const double inverse =
CertifiedInverse(p.assembly.Conductivity(), partition.free);
double residual = 0, coefficient_error = 0;
for (auto i : partition.free) {
residual = std::max(residual, result.certified_residual[i]);
I row;
for (std::size_t k = p.assembly.ConductivityError().RowOffsets()[i];
k < p.assembly.ConductivityError().RowOffsets()[i + 1]; ++k)
row = row + I{p.assembly.ConductivityError().Values()[k]};
coefficient_error = std::max(coefficient_error, row.hi);
}
const I denominator = I{1} - I{inverse} * I{coefficient_error};
ASSERT_GT(denominator.lo, 0);
const I bound = I{inverse} / denominator * I{residual};
const I mid = I{5} / I{6}; // R=1/(1*1*1)+1/(10*.5*1)=6/5.
for (auto i : partition.free)
EXPECT_LE((I{result.temperature[i]} - mid).Abs(),
bound.hi + (mid - I{mid.Mid()}).Abs());
I left, right;
for (std::size_t i = 0; i < partition.constrained.size(); ++i)
if (partition.constrained[i] / 5 >= 4)
right = right + I{result.reaction[i]};
else
left = left + I{result.reaction[i]};
I reaction_budget;
for (auto i : partition.constrained) {
I row, scale, error;
for (std::size_t k = p.assembly.Conductivity().RowOffsets()[i];
k < p.assembly.Conductivity().RowOffsets()[i + 1]; ++k) {
const auto j = p.assembly.Conductivity().ColumnIndices()[k];
row = row + I{std::abs(p.assembly.Conductivity().Values()[k])} * bound;
scale = scale + I{std::abs(p.assembly.Conductivity().Values()[k])} *
I{std::abs(result.temperature[j])};
error = error + I{p.assembly.ConductivityError().Values()[k]} *
I{std::abs(result.temperature[j])};
}
const auto terms = p.assembly.Conductivity().RowOffsets()[i + 1] -
p.assembly.Conductivity().RowOffsets()[i];
reaction_budget =
reaction_budget + row + error + Gamma(2 * terms + 2) * scale;
}
EXPECT_LE((right - mid).Abs(), reaction_budget.hi);
EXPECT_LE((left + mid).Abs(), reaction_budget.hi);
EXPECT_LE((left + right).Abs(), result.evidence.energy_bound);
auto flux = Take(p.assembly.RecoverFlux(result.temperature, parallel));
for (const auto& q : flux[0])
EXPECT_LE((I{q.X()} + mid).Abs(), 24 * bound.hi + Gamma(2048).hi * 24);
for (const auto& q : flux[1]) {
const double component = folded ? q.Z() : q.X();
EXPECT_LE((I{component} + I{5} / I{3}).Abs(),
400 * bound.hi + Gamma(2048).hi * 400);
}
std::cout << std::setprecision(17) << "THERMAL_STRIP folded=" << folded
<< " resistance=1.2 reaction=" << right.Mid()
<< " forward_bound=" << bound.hi
<< " beta=" << result.evidence.beta_cert << '\n';
}
}
TEST(ThermalPhysics,
PrivateAffineFieldRotationNormalAndUnaveragedMidplaneTraces) {
fesa::SerialParallelFor parallel;
const auto rotate = [](Vector3 v) { return Vector3{v.Z(), v.X(), v.Y()}; };
for (double h : {.5, 1., 2.})
for (double k : {1., 10.})
for (bool flip : {false, true})
for (bool rotated : {false, true}) {
SCOPED_TRACE(h);
SCOPED_TRACE(k);
SCOPED_TRACE(flip);
SCOPED_TRACE(rotated);
auto m = Mesh(1, 1, 1, [](auto, auto) { return 0.; });
m.thermal_sections[0].thickness = h;
m.thermal_materials[0].conductivity = k;
if (flip) m.thermal_elements[0].node_indices = {{0, 2, 3, 1}};
Vector values{20};
for (std::size_t node = 0; node < 4; ++node) {
const auto& r = m.nodes[node].coordinates;
for (std::size_t station = 0; station < 5; ++station) {
const double zeta = -1 + .5 * station;
values[5 * node + station] =
2 + 3 * r[0] - 2 * r[1] + 4 * (flip ? -1 : 1) * h * zeta / 2;
}
if (rotated) {
const auto r_new = rotate(Vector3{r[0], r[1], r[2]});
m.nodes[node].coordinates = {{r_new.X(), r_new.Y(), r_new.Z()}};
}
}
Prepared p{std::move(m), parallel};
ti::ThermalPartition partition;
for (std::size_t i = 0; i < 20; ++i)
partition.constrained.push_back(i);
partition.prescribed = values;
fesa::MklPardisoSolver solver;
ti::ThermalFactorization cache;
const auto result = Take(ti::SolveThermalIncrement(
{p.assembly.Conductivity(), p.assembly.Capacity(),
p.assembly.HeatInput()},
partition, values, 1, solver, cache));
EXPECT_FALSE(cache.matrix.has_value());
const auto flux =
Take(p.assembly.RecoverFlux(result.temperature, parallel));
Vector3 expected{-3 * k, 2 * k, -4 * k};
if (rotated) expected = rotate(expected);
double scale = 0;
for (std::size_t i = 0; i < 20; ++i)
scale = std::max(scale, std::abs(values[i]));
const double budget = (Gamma(2048) * I{k * (8 + 16 / h) * scale}).hi;
for (std::size_t point = 0; point < 4; ++point) {
const auto& r = p.state.FluxLocalFrames()[0][point];
for (std::size_t a = 0; a < 3; ++a)
for (std::size_t b = 0; b < 3; ++b) {
I dot, absolute;
for (std::size_t c = 0; c < 3; ++c) {
dot = dot + I{r[a][c]} * I{r[b][c]};
absolute = absolute + I{std::abs(r[a][c] * r[b][c])};
}
EXPECT_LE((dot - I{static_cast<double>(a == b)}).Abs(),
(Gamma(2048) * absolute).hi);
}
EXPECT_GT(r[0].Cross(r[1]).Dot(r[2]), 0);
for (std::size_t trace = 0; trace < 6; ++trace) {
const auto& q = flux[0][6 * point + trace];
for (std::size_t c = 0; c < 3; ++c)
EXPECT_LE(std::abs(q[c] - expected[c]), budget);
for (std::size_t axis = 0; axis < 3; ++axis) {
I actual_local, expected_local;
for (std::size_t c = 0; c < 3; ++c) {
actual_local = actual_local + I{r[axis][c]} * I{q[c]};
expected_local =
expected_local + I{r[axis][c]} * I{expected[c]};
}
EXPECT_LE((actual_local - expected_local).Abs(),
budget + Gamma(16).hi * k * (8 + 16 / h) * scale);
}
}
}
// The discontinuous derivative at the shared station has two distinct
// one-sided traces; prescribed interior stations exist only in this
// private algebra fixture, never in the public Domain BC definition.
for (std::size_t node = 0; node < 4; ++node) {
for (std::size_t station = 0; station < 5; ++station)
values[5 * node + station] =
station < 3 ? 0. : static_cast<double>(station - 2);
}
partition.prescribed = values;
const auto trace_result = Take(ti::SolveThermalIncrement(
{p.assembly.Conductivity(), p.assembly.Capacity(),
p.assembly.HeatInput()},
partition, values, 1, solver, cache));
const auto traces =
Take(p.assembly.RecoverFlux(trace_result.temperature, parallel));
for (std::size_t point = 0; point < 4; ++point) {
const auto& normal = p.state.FluxLocalFrames()[0][point][2];
EXPECT_EQ(traces[0][6 * point + 2].Dot(normal), 0);
EXPECT_LE(
std::abs(traces[0][6 * point + 3].Dot(normal) + 4 * k / h),
(Gamma(2048) * I{2 * k * (8 + 16 / h)}).hi);
}
}
}
I ThicknessMass(const std::array<I, 5>& v) {
I result;
for (std::size_t segment = 0; segment < 2; ++segment) {
const I m = (v[2 * segment] + v[2 * segment + 2]) / I{2};
const I d = (v[2 * segment + 2] - v[2 * segment]) / I{2};
const I middle = v[2 * segment + 1];
// Positive square decomposition of the exact two-P2 consistent mass.
result = result + (I{6} * Square(m + I{2} / I{3} * middle) +
I{40} / I{3} * Square(middle) + I{10} * Square(d)) /
I{60};
}
return result;
}
I ThicknessGradient(const std::array<I, 5>& v) {
I result;
for (std::size_t segment = 0; segment < 2; ++segment) {
const I left = v[2 * segment] - v[2 * segment + 1],
right = v[2 * segment + 2] - v[2 * segment + 1];
result = result +
I{2} / I{3} *
(I{6} * (Square(left) + Square(right)) + Square(left + right));
}
return result;
}
struct SpatialError {
I temperature, gradient;
};
template <std::size_t Order>
SpatialError IntegrateSpatialError(
const Vector& values, std::size_t n,
const std::array<thermal_oracle::GaussPoint, Order>& rule, I amplitude) {
I temperature, gradient;
for (std::size_t y = 0; y < n; ++y)
for (std::size_t x = 0; x < n; ++x) {
const std::size_t node = y * (n + 1) + x;
const std::array<std::size_t, 4> nodes{node, node + 1, node + n + 2,
node + n + 1};
for (const auto& gx : rule) {
const I u = (I{1} + I{gx.x_lo, gx.x_hi}) / I{2};
const I position =
(I{static_cast<double>(x)} + u) / I{static_cast<double>(n)};
const I phase = Pi() * position;
const I exact = Cos(phase) * amplitude;
const I exact_gradient =
I{-1} * Pi() * Cos(Pi() / I{2} - phase) * amplitude;
for (const auto& gy : rule) {
const I v = (I{1} + I{gy.x_lo, gy.x_hi}) / I{2};
const I weight =
I{gx.w_lo, gx.w_hi} * I{gy.w_lo, gy.w_hi} / I{4. * n * n};
std::array<I, 5> field, dx, dy, error;
for (std::size_t a = 0; a < 5; ++a) {
const I a0{values[5 * nodes[0] + a]}, a1{values[5 * nodes[1] + a]},
a2{values[5 * nodes[2] + a]}, a3{values[5 * nodes[3] + a]};
field[a] = (I{1} - v) * ((I{1} - u) * a0 + u * a1) +
v * ((I{1} - u) * a3 + u * a2);
error[a] = field[a] - exact;
dx[a] = I{static_cast<double>(n)} *
((I{1} - v) * (a1 - a0) + v * (a2 - a3)) -
exact_gradient;
dy[a] = I{static_cast<double>(n)} *
((I{1} - u) * (a3 - a0) + u * (a2 - a1));
}
temperature = temperature + weight * ThicknessMass(error);
gradient =
gradient + weight * (ThicknessMass(dx) + ThicknessMass(dy) +
ThicknessGradient(field));
}
}
}
return {Sqrt(temperature), Sqrt(gradient)};
}
TEST(ThermalPhysics,
FullTwoDimensionalMeshesHaveSecondOrderTemperatureAndFirstOrderGradient) {
fesa::SerialParallelFor parallel;
constexpr double period = .001;
constexpr std::uint32_t count = 32;
const I continuum_amplitude = ExpNegative(Pi() * Pi() * I{period});
double previous_temperature = 0, previous_gradient = 0;
for (std::size_t n : {8U, 16U, 32U, 64U}) {
SCOPED_TRACE(n);
std::vector<I> mode(n + 1);
double initial_error = 0;
for (std::size_t i = 0; i <= n; ++i) {
mode[i] =
Cos(Pi() * I{static_cast<double>(i)} / I{static_cast<double>(n)});
initial_error =
std::max(initial_error, (mode[i] - I{mode[i].Mid()}).Abs());
}
Prepared p{Mesh(n, period, count,
[&](std::size_t i, auto) { return mode[i].Mid(); }),
parallel};
const auto metrics =
Advance(p, parallel, Down(1. / (2160 * n * n)), initial_error);
const auto& actual = p.state.CommittedState().temperature;
const auto twelve = IntegrateSpatialError(
actual, n, thermal_oracle::kGauss12, continuum_amplitude);
const auto sixteen = IntegrateSpatialError(
actual, n, thermal_oracle::kGauss16, continuum_amplitude);
EXPECT_LE((twelve.temperature - sixteen.temperature).Abs(),
sixteen.temperature.lo * 1e-8);
EXPECT_LE((twelve.gradient - sixteen.gradient).Abs(),
sixteen.gradient.lo * 1e-8);
const I lambda = I{6. * n * n} * (I{1} - mode[1]) / (I{2} + mode[1]);
const I semidiscrete = ExpNegative(lambda * I{period});
const I be = Power(I{1} / (I{1} + lambda * I{period / count}), count);
Vector spatial{actual.Size()}, difference{actual.Size()},
mode_values{actual.Size()};
double oracle_width = 0;
for (std::size_t node = 0; node < (n + 1) * (n + 1); ++node)
for (std::size_t a = 0; a < 5; ++a) {
const auto i = node % (n + 1);
const I expected = mode[i] * be;
spatial[5 * node + a] = (mode[i] * semidiscrete).Mid();
mode_values[5 * node + a] = mode[i].Mid();
difference[5 * node + a] = actual[5 * node + a] - expected.Mid();
oracle_width =
std::max(oracle_width, (expected - I{expected.Mid()}).Abs());
}
const I agreement = CapacityNorm(difference, n);
const I forward = I{metrics.capacity_budget} + I{oracle_width};
EXPECT_LE(agreement.hi, forward.hi);
EXPECT_LE(forward.hi, sixteen.temperature.lo / 1000);
EXPECT_LE((Sqrt(I{24. * n * n + 1280}) * forward).hi,
sixteen.gradient.lo / 1000);
const auto spatial_error = IntegrateSpatialError(
spatial, n, thermal_oracle::kGauss12, continuum_amplitude);
const I temporal =
I{(be - semidiscrete).Abs()} * CapacityNorm(mode_values, n);
EXPECT_LE(temporal.hi, spatial_error.temperature.lo / 32);
double temperature_order = 0, gradient_order = 0;
if (previous_temperature > 0) {
temperature_order =
std::log2(previous_temperature / sixteen.temperature.Mid());
gradient_order = std::log2(previous_gradient / sixteen.gradient.Mid());
EXPECT_GE(temperature_order, 1.95);
EXPECT_LE(temperature_order, 2.05);
EXPECT_GE(gradient_order, .98);
EXPECT_LE(gradient_order, 1.03);
}
previous_temperature = sixteen.temperature.Mid();
previous_gradient = sixteen.gradient.Mid();
Print("space", n, count, metrics);
std::cout << std::setprecision(17) << "THERMAL_SPACE n=" << n
<< " T_error=" << sixteen.temperature.Mid()
<< " gradient_error=" << sixteen.gradient.Mid()
<< " T_order=" << temperature_order
<< " gradient_order=" << gradient_order << " time_spatial_ratio="
<< (temporal / spatial_error.temperature).hi
<< " quadrature_T_gap="
<< (twelve.temperature - sixteen.temperature).Abs()
<< " quadrature_gradient_gap="
<< (twelve.gradient - sixteen.gradient).Abs()
<< " modal_agreement=" << agreement.hi
<< " roundoff_budget=" << forward.hi << '\n';
if (n == 64)
EXPECT_EQ(8 * (metrics.temperature_values + metrics.flux_values),
81074472U);
}
}
I Bracket(thermal_oracle::Bracket x) { return {x.lo, x.hi}; }
class ThermalSlab
: public testing::TestWithParam<std::pair<int, std::uint32_t>> {};
TEST_P(ThermalSlab, ActualMeshesMatchCertifiedP2AndSeparateFourierError) {
const auto [case_index, count] = GetParam();
const auto& oracle = thermal_oracle::kSlab[case_index];
const auto& be = count == 4096 ? oracle.be4096 : oracle.be8192;
fesa::SerialParallelFor parallel;
std::array<I, 13> first_mesh;
std::array<double, 4> first_budget{};
for (std::size_t n : {1U, 2U}) {
auto definition =
Mesh(n, oracle.time, count, [](auto, auto) { return 0.; });
for (std::size_t node = 0; node < definition.nodes.size(); ++node)
definition.thermal_model_boundaries.push_back(
{static_cast<fesa::EntityIndex>(node), 11, 1, {}});
Prepared p{std::move(definition), parallel};
const double mass_lower = Down(1. / (2160 * n * n));
for (std::size_t row = 0; row < p.state.CommittedState().temperature.Size();
++row)
EXPECT_EQ(p.state.CommittedState().temperature[row], 0);
const auto metrics = Advance(p, parallel, mass_lower);
const auto& final = p.state.CommittedState();
// Independently certify the small last-step inverse as well as the
// structured C-norm propagation used for the full 4096/8192-step history.
const auto& c = p.assembly.Capacity();
const auto& k = p.assembly.Conductivity();
const double delta = final.time_increment;
std::vector<fesa::CooContribution> coo;
double operator_error = 0;
for (std::size_t row = 0; row < c.Rows(); ++row) {
I error;
for (auto entry = c.RowOffsets()[row]; entry < c.RowOffsets()[row + 1];
++entry) {
const double center = c.Values()[entry] / delta + k.Values()[entry];
coo.push_back({row, c.ColumnIndices()[entry], center, 0, entry});
const I exact = (I{c.Values()[entry]} +
I{-p.assembly.CapacityError().Values()[entry],
p.assembly.CapacityError().Values()[entry]}) /
I{delta} +
I{k.Values()[entry]} +
I{-p.assembly.ConductivityError().Values()[entry],
p.assembly.ConductivityError().Values()[entry]};
error = error + I{(exact - I{center}).Abs()};
}
operator_error = std::max(operator_error, error.hi);
}
const auto a =
Take(fesa::SparseMatrix::FromCoo(c.Rows(), c.Columns(), std::move(coo),
{c.RowOffsets(), c.ColumnIndices()}));
const double inverse = CertifiedInverse(a, p.dofs.FreeDofs());
const I inverse_denominator = I{1} - I{inverse} * I{operator_error};
ASSERT_GT(inverse_denominator.lo, 0);
const I local_forward =
I{inverse} / inverse_denominator * I{metrics.last_free_residual_inf};
const I temperature_budget =
I{metrics.capacity_budget} / Sqrt(I{mass_lower});
double temperature_scale = 0;
for (std::size_t row = 0; row < final.temperature.Size(); ++row)
temperature_scale =
std::max(temperature_scale, std::abs(final.temperature[row]));
const I flux_budget = I{8. * n + 16} * (temperature_budget +
Gamma(2048) * I{temperature_scale});
I energy, reaction;
constexpr int thickness_weight[5] = {1, 4, 2, 4, 1};
for (std::size_t j = 0; j <= n; ++j)
for (std::size_t i = 0; i <= n; ++i) {
const int plane_weight =
(i == 0 || i == n ? 1 : 2) * (j == 0 || j == n ? 1 : 2);
for (std::size_t a = 0; a < 5; ++a)
energy =
energy +
I{final.temperature[5 * (j * (n + 1) + i) + a]} *
I{static_cast<double>(plane_weight * thickness_weight[a])} /
I{48. * n * n};
}
for (std::size_t row = 0; row < final.completed->reaction_heat_rate.Size();
++row)
reaction = reaction + I{final.completed->reaction_heat_rate[row]};
// Exact P2 Schur reaction row L = (-400/51,56/17,-80/51,28/51),
// L Cff^-1 L^T = 6447104/14739. Plane integration is a contraction
// in the product C norm; Ccf Cff^-1 has infinity norm 3/17.
const I reaction_budget =
Sqrt(I{6447104} / I{14739}) * I{metrics.capacity_budget} +
(I{3} / I{17}) * I{metrics.last_free_residual_l1} +
I{metrics.last_reaction_evaluation_error} +
I{(reaction - I{reaction.Mid()}).Abs()};
const std::array<double, 4> solver_budget{
temperature_budget.hi, flux_budget.hi,
(I{metrics.capacity_budget} + I{(energy - I{energy.Mid()}).Abs()}).hi,
reaction_budget.hi};
std::array<double, 4> agreement{}, physical{}, oracle_budget{},
time_error{}, space_error{};
const auto observe = [&](std::size_t quantity, std::size_t group, I value) {
agreement[group] =
std::max(agreement[group], (value - Bracket(be[quantity])).Abs());
physical[group] = std::max(
physical[group], (value - Bracket(oracle.fourier[quantity])).Abs());
};
for (std::size_t quantity = 0; quantity < 13; ++quantity) {
const std::size_t group = quantity < 5 ? 0
: quantity < 11 ? 1
: quantity - 9;
oracle_budget[group] = std::max(
oracle_budget[group],
(I{be[quantity].hi} - I{be[quantity].lo} +
I{oracle.continuous[quantity].hi} -
I{oracle.continuous[quantity].lo} + I{oracle.fourier[quantity].hi} -
I{oracle.fourier[quantity].lo})
.hi);
time_error[group] = std::max(
time_error[group],
(Bracket(be[quantity]) - Bracket(oracle.continuous[quantity])).Abs());
space_error[group] =
std::max(space_error[group], (Bracket(oracle.continuous[quantity]) -
Bracket(oracle.fourier[quantity]))
.Abs());
}
for (std::size_t row = 0; row < final.temperature.Size(); ++row)
observe(row % 5, 0, I{final.temperature[row]});
for (const auto& element : final.completed->global_heat_flux)
for (std::size_t sample = 0; sample < 24; ++sample) {
EXPECT_LE(std::abs(element[sample][0]), flux_budget.hi);
EXPECT_LE(std::abs(element[sample][1]), flux_budget.hi);
observe(5 + sample % 6, 1, I{element[sample][2]});
}
observe(11, 2, energy);
observe(12, 3, reaction);
std::array<I, 13> samples;
for (std::size_t a = 0; a < 5; ++a) samples[a] = I{final.temperature[a]};
for (std::size_t trace = 0; trace < 6; ++trace)
samples[5 + trace] = I{final.completed->global_heat_flux[0][trace][2]};
samples[11] = energy;
samples[12] = reaction;
if (n == 1) {
first_mesh = samples;
first_budget = solver_budget;
} else {
for (std::size_t quantity = 0; quantity < 13; ++quantity) {
const std::size_t group = quantity < 5 ? 0
: quantity < 11 ? 1
: quantity - 9;
EXPECT_LE((samples[quantity] - first_mesh[quantity]).Abs(),
(I{first_budget[group]} + I{solver_budget[group]}).hi);
}
}
Print("slab", n, count, metrics);
for (std::size_t group = 0; group < 4; ++group) {
const I roundoff = I{solver_budget[group]} + I{oracle_budget[group]};
EXPECT_LE(roundoff.hi, oracle.space[group] / 1000);
EXPECT_LE(agreement[group], roundoff.hi);
EXPECT_LE(physical[group],
(I{oracle.space[group]} + I{time_error[group]} + roundoff).hi);
EXPECT_LE((I{physical[group]} - I{space_error[group]}).Abs(),
(I{time_error[group]} + roundoff).hi);
const bool plateau =
(I{time_error[group]} + roundoff).hi <= space_error[group] / 10;
if (case_index < 4 && count == 8192) EXPECT_TRUE(plateau);
std::cout << std::setprecision(17) << "THERMAL_SLAB time=" << oracle.time
<< " n=" << n << " N=" << count << " quantity=" << group
<< " finite_BE_gap=" << agreement[group]
<< " solver_bound=" << solver_budget[group]
<< " oracle_bound=" << oracle_budget[group]
<< " physical_error=" << physical[group]
<< " space_error=" << space_error[group]
<< " D_space=" << oracle.space[group]
<< " D_time=" << time_error[group] << " plateau=" << plateau
<< " last_step_inverse_bound=" << inverse
<< " last_step_forward_bound=" << local_forward.hi
<< " penetration=" << std::sqrt(oracle.time)
<< " station_spacing=0.25 segment_length=0.5" << '\n';
}
}
}
INSTANTIATE_TEST_SUITE_P(
FixedStations, ThermalSlab,
testing::Values(std::make_pair(0, 4096U), std::make_pair(0, 8192U),
std::make_pair(1, 4096U), std::make_pair(1, 8192U),
std::make_pair(2, 4096U), std::make_pair(2, 8192U),
std::make_pair(3, 4096U), std::make_pair(3, 8192U),
std::make_pair(4, 4096U), std::make_pair(4, 8192U),
std::make_pair(5, 4096U), std::make_pair(5, 8192U)));
} // namespace