1142 lines
51 KiB
C++
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
|