feat(thermal): assemble heat operators and preserve typed frame history

This commit is contained in:
KOKO\Mimi
2026-09-29 20:09:13 +09:00
parent 85b5daac89
commit 54154bee15
7 changed files with 1462 additions and 0 deletions
@@ -0,0 +1,301 @@
#include "fesa/analysis/thermal_analysis_state.h"
#include <gtest/gtest.h>
#include <algorithm>
#include <cstddef>
#include <cstring>
#include <functional>
#include <limits>
#include <string>
#include <utility>
#include <vector>
namespace {
fesa::ModelDefinition Definition(bool constrained = true, double period = .9,
double increment = .3,
std::uint32_t maximum = 3) {
fesa::ModelDefinition model{};
model.nodes = {{{"unused", 99, "0099"}, {8, 8, 8}, {}},
{{"plate", 7, "0007"}, {0, 0, 0}, {}},
{{"plate", 2, "0002"}, {1, 0, 0}, {}},
{{"plate", 9, "0009"}, {1, 1, 0}, {}},
{{"plate", 1, "0001"}, {0, 1, 0}, {}}};
model.thermal_materials = {{{"plate", 1, "1"}, "heat", 1, 1, 1, {}}};
model.thermal_sections = {{{"plate", 1, "1"}, "section", 2, 0, {}}};
model.thermal_elements = {{{"plate", 13, "0013"}, {{1, 2, 3, 4}}, 0, {}}};
model.thermal_initial_conditions = {{1, -20, {}}, {4, 7.125, {}}};
model.thermal_step =
fesa::ThermalStepDefinition{"named-step", increment, period, maximum};
if (constrained) {
model.thermal_model_boundaries = {{1, 11, 100, {}}, {4, 15, 20, {}}};
model.thermal_step->boundaries = {{1, 11, 200, {}}};
}
return model;
}
// Domain, model, DOFs, and assembly all die before any returned state is
// consumed.
fesa::Result<fesa::ThermalAnalysisState> State(bool constrained = true,
double period = .9,
double increment = .3,
std::uint32_t maximum = 3) {
auto domain =
fesa::Domain::Create(Definition(constrained, period, increment, maximum));
if (!domain.HasValue())
return fesa::Result<fesa::ThermalAnalysisState>::Failure(
domain.GetStatus());
auto model = fesa::AnalysisModel::Create(domain.Value());
if (!model.HasValue())
return fesa::Result<fesa::ThermalAnalysisState>::Failure(model.GetStatus());
auto dofs = fesa::DofManager::CreateThermal(model.Value());
if (!dofs.HasValue())
return fesa::Result<fesa::ThermalAnalysisState>::Failure(dofs.GetStatus());
auto assembly = fesa::AssembleThermal(model.Value(), dofs.Value(),
fesa::SerialParallelFor{});
if (!assembly.HasValue())
return fesa::Result<fesa::ThermalAnalysisState>::Failure(
assembly.GetStatus());
return fesa::ThermalAnalysisState::Create(model.Value(), dofs.Value(),
assembly.Value());
}
fesa::Result<fesa::ThermalAcceptedFrame> Candidate(
const fesa::ThermalAnalysisState& state, std::uint32_t index) {
fesa::Vector temperature{state.NodeOrder().size() * 5};
for (std::size_t i = 0; i < temperature.Size(); ++i)
temperature[i] = static_cast<double>(i) - 4.5;
fesa::Vector reaction{state.ReactionDofs().size(), -3};
std::vector<fesa::ThermalElementFlux> flux(state.ElementOrder().size());
for (std::size_t e = 0; e < flux.size(); ++e)
for (std::size_t p = 0; p < 24; ++p)
flux[e][p] = fesa::Vector3{static_cast<double>(p), -2, 3};
return state.MakeCompletedFrame(index, std::move(temperature),
std::move(reaction), std::move(flux));
}
std::vector<std::uint64_t> Snapshot(const fesa::ThermalAnalysisState& state) {
std::vector<std::uint64_t> result;
const auto scalar = [&](double x) {
std::uint64_t bits = 0;
std::memcpy(&bits, &x, sizeof(bits));
result.push_back(bits);
};
const auto vector = [&](const fesa::Vector& v) {
result.push_back(v.Size());
for (std::size_t i = 0; i < v.Size(); ++i) scalar(v[i]);
};
for (const auto& frame : state.AcceptedFrames()) {
result.insert(result.end(),
{frame.frame_index, frame.increment_index, frame.is_initial,
frame.completed.has_value()});
scalar(frame.time);
scalar(frame.time_increment);
result.push_back(frame.node_order.size());
for (const auto node : frame.node_order) result.push_back(node);
vector(frame.temperature);
if (frame.completed) {
result.push_back(frame.completed->reaction_dofs.size());
for (const auto& dof : frame.completed->reaction_dofs) {
result.push_back(dof.node);
result.push_back(dof.source_dof);
}
vector(frame.completed->reaction_heat_rate);
result.push_back(frame.completed->element_order.size());
for (const auto e : frame.completed->element_order) result.push_back(e);
result.push_back(frame.completed->global_heat_flux.size());
for (const auto& element : frame.completed->global_heat_flux)
for (const auto& flux : element)
for (const auto value : flux.Components()) scalar(value);
}
}
return result;
}
} // namespace
TEST(ThermalAnalysisState,
OwnsRawInitialInventoryAfterDomainAndAssemblyDestruction) {
auto state = State();
ASSERT_TRUE(state.HasValue());
EXPECT_EQ(state.Value().NodeOrder(),
(std::vector<fesa::EntityIndex>{1, 2, 3, 4}));
EXPECT_EQ(state.Value().NodeSourceIds()[0].source_label_text, "0007");
EXPECT_EQ(state.Value().ElementSourceIds()[0].source_label_text, "0013");
EXPECT_EQ(state.Value().ElementOrder(), (std::vector<fesa::EntityIndex>{0}));
ASSERT_EQ(state.Value().AcceptedFrames().size(), 1U);
const auto& initial = state.Value().CommittedState();
EXPECT_TRUE(initial.is_initial);
EXPECT_FALSE(initial.completed.has_value());
EXPECT_EQ(initial.time, 0);
EXPECT_EQ(initial.time_increment, 0);
for (std::size_t a = 0; a < 5; ++a) {
EXPECT_EQ(initial.temperature[a], -20.);
EXPECT_EQ(initial.temperature[5 + a], 0.);
EXPECT_EQ(initial.temperature[10 + a], 0.);
EXPECT_EQ(initial.temperature[15 + a], 7.125);
}
ASSERT_EQ(state.Value().ReactionDofs().size(), 2U);
EXPECT_EQ(state.Value().ReactionDofs()[0].node, 1U);
EXPECT_EQ(state.Value().ReactionDofs()[0].source_dof, 11);
EXPECT_EQ(state.Value().ReactionDofs()[1].node, 4U);
EXPECT_EQ(state.Value().ReactionDofs()[1].source_dof, 15);
for (std::size_t row = 0; row < 4; ++row)
for (std::size_t a = 0; a < 5; ++a)
EXPECT_EQ(state.Value().ConstraintMask()[row][a],
((row == 0 && a == 0) || (row == 3 && a == 4)) ? 1 : 0);
ASSERT_EQ(state.Value().FluxLocalFrames().size(), 1U);
for (const auto& frame : state.Value().FluxLocalFrames()[0]) {
EXPECT_EQ(frame[0], (fesa::Vector3{1, 0, 0}));
EXPECT_EQ(frame[1], (fesa::Vector3{0, 1, 0}));
EXPECT_EQ(frame[2], (fesa::Vector3{0, 0, 1}));
}
}
TEST(ThermalAnalysisState, CompletedZeroReactionPayloadAndExactActualDelta) {
auto state = State(false);
ASSERT_TRUE(state.HasValue());
EXPECT_TRUE(state.Value().ReactionDofs().empty());
EXPECT_FALSE(state.Value().IsComplete());
for (std::uint32_t i = 1; i <= 3; ++i) {
auto frame = Candidate(state.Value(), i);
ASSERT_TRUE(frame.HasValue());
ASSERT_TRUE(frame.Value().completed.has_value());
EXPECT_EQ(frame.Value().completed->reaction_heat_rate.Size(), 0U);
EXPECT_EQ(frame.Value().time_increment, .3);
ASSERT_TRUE(state.Value().CommitAcceptedFrame(frame.Value()).IsOk());
}
EXPECT_TRUE(state.Value().IsComplete());
EXPECT_EQ(state.Value().AcceptedFrames().size(), 4U);
EXPECT_EQ(state.Value().CommittedState().time, .9);
EXPECT_EQ(state.Value().CommittedState().time_increment, .3);
EXPECT_NE(state.Value().CommittedState().time -
state.Value().AcceptedFrames()[2].time,
.3);
EXPECT_EQ(state.Value().NominalTimeIncrement(), .3);
EXPECT_EQ(state.Value().TimePeriod(), .9);
EXPECT_FALSE(
state.Value().CommitAcceptedFrame(state.Value().CommittedState()).IsOk());
auto short_state = State(false, 10, 3, 4);
ASSERT_TRUE(short_state.HasValue());
auto last = Candidate(short_state.Value(), 4);
ASSERT_TRUE(last.HasValue());
EXPECT_EQ(last.Value().time, 10.);
EXPECT_EQ(last.Value().time_increment, 1.);
}
TEST(ThermalAnalysisState,
EveryRejectedCandidatePreservesEntireAcceptedHistory) {
auto created = State();
ASSERT_TRUE(created.HasValue());
auto& state = created.Value();
auto first = Candidate(state, 1);
ASSERT_TRUE(first.HasValue());
ASSERT_TRUE(state.CommitAcceptedFrame(first.Value()).IsOk());
const auto snapshot = Snapshot(state);
const std::vector<std::function<void(fesa::ThermalAcceptedFrame&)>>
corruptions{
[](auto& f) {
f.frame_index = 3;
f.increment_index = 3;
},
[](auto& f) { f.increment_index = 1; },
[](auto& f) { f.is_initial = true; },
[](auto& f) { f.time += .01; },
[](auto& f) { f.time_increment = .2; },
[](auto& f) { f.time = std::numeric_limits<double>::quiet_NaN(); },
[](auto& f) {
f.time_increment = std::numeric_limits<double>::infinity();
},
[](auto& f) { f.node_order.pop_back(); },
[](auto& f) { f.node_order.push_back(0); },
[](auto& f) { std::swap(f.node_order[0], f.node_order[1]); },
[](auto& f) { f.node_order[1] = f.node_order[0]; },
[](auto& f) { f.temperature = fesa::Vector{19}; },
[](auto& f) {
f.temperature[0] = std::numeric_limits<double>::quiet_NaN();
},
[](auto& f) { f.completed.reset(); },
[](auto& f) { f.completed->reaction_dofs.pop_back(); },
[](auto& f) { f.completed->reaction_dofs[0].source_dof = 12; },
[](auto& f) {
std::swap(f.completed->reaction_dofs[0],
f.completed->reaction_dofs[1]);
},
[](auto& f) {
f.completed->reaction_dofs[1] = f.completed->reaction_dofs[0];
},
[](auto& f) { f.completed->reaction_heat_rate = fesa::Vector{3}; },
[](auto& f) {
f.completed->reaction_heat_rate[0] =
std::numeric_limits<double>::infinity();
},
[](auto& f) { f.completed->element_order[0] = 1; },
[](auto& f) { f.completed->element_order.push_back(0); },
[](auto& f) { f.completed->global_heat_flux.clear(); },
[](auto& f) {
f.completed->global_heat_flux[0][9] = {
0, 0, std::numeric_limits<double>::quiet_NaN()};
}};
for (const auto& corrupt : corruptions) {
auto candidate = Candidate(state, 2);
ASSERT_TRUE(candidate.HasValue());
corrupt(candidate.Value());
EXPECT_FALSE(state.CommitAcceptedFrame(candidate.Value()).IsOk());
EXPECT_EQ(Snapshot(state), snapshot);
}
auto second = Candidate(state, 2);
ASSERT_TRUE(second.HasValue());
ASSERT_TRUE(state.CommitAcceptedFrame(second.Value()).IsOk());
second.Value().temperature[0] = 900;
EXPECT_NE(state.CommittedState().temperature[0], 900);
}
TEST(ThermalAnalysisState,
CheckedCandidateRejectsMissingDataAndHugeGridDoesNotPreallocateHistory) {
auto state = State(false, 4294967295., 1., 4294967295U);
ASSERT_TRUE(state.HasValue());
EXPECT_EQ(state.Value().TimeGrid().Count(), 4294967295U);
EXPECT_EQ(state.Value().AcceptedFrames().size(), 1U);
EXPECT_FALSE(state.Value().IsComplete());
auto last = Candidate(state.Value(), 4294967295U);
ASSERT_TRUE(last.HasValue());
EXPECT_EQ(last.Value().frame_index, 4294967295ULL);
EXPECT_FALSE(state.Value().CommitAcceptedFrame(last.Value()).IsOk());
EXPECT_FALSE(
state.Value()
.MakeCompletedFrame(0, fesa::Vector{20}, fesa::Vector{0}, {{}})
.HasValue());
EXPECT_FALSE(
state.Value()
.MakeCompletedFrame(1, fesa::Vector{19}, fesa::Vector{0}, {{}})
.HasValue());
EXPECT_FALSE(
state.Value()
.MakeCompletedFrame(1, fesa::Vector{20}, fesa::Vector{1}, {{}})
.HasValue());
EXPECT_FALSE(state.Value()
.MakeCompletedFrame(1, fesa::Vector{20}, fesa::Vector{0}, {})
.HasValue());
}
TEST(ThermalAnalysisState, RejectsAssemblyFromAnotherDomainAndInvalidSchedule) {
auto first = fesa::Domain::Create(Definition());
ASSERT_TRUE(first.HasValue());
auto second = fesa::Domain::Create(Definition());
ASSERT_TRUE(second.HasValue());
auto fm = fesa::AnalysisModel::Create(first.Value());
auto sm = fesa::AnalysisModel::Create(second.Value());
ASSERT_TRUE(fm.HasValue());
ASSERT_TRUE(sm.HasValue());
auto fd = fesa::DofManager::CreateThermal(fm.Value());
auto sd = fesa::DofManager::CreateThermal(sm.Value());
ASSERT_TRUE(fd.HasValue());
ASSERT_TRUE(sd.HasValue());
auto assembly =
fesa::AssembleThermal(fm.Value(), fd.Value(), fesa::SerialParallelFor{});
ASSERT_TRUE(assembly.HasValue());
EXPECT_FALSE(fesa::ThermalAnalysisState::Create(sm.Value(), sd.Value(),
assembly.Value())
.HasValue());
EXPECT_FALSE(State(false, 10, 3, 3).HasValue());
}
@@ -0,0 +1,323 @@
#include "fesa/assembly/thermal_assembler.h"
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstring>
#include <limits>
#include <string>
#include <utility>
#include <vector>
namespace {
fesa::ModelDefinition Model() {
fesa::ModelDefinition model{};
const std::array<std::array<double, 3>, 7> points{{{90, 90, 90},
{0, 0, 0},
{1, 0, 0},
{2, 0, 0},
{0, 1, 0},
{1, 1, 0},
{2, 1, 0}}};
const std::array<int, 7> labels{{99, 70, 3, 80, 1, 40, 7}};
for (std::size_t i = 0; i < points.size(); ++i)
model.nodes.push_back({{"plate", labels[i], std::to_string(labels[i])},
points[i],
{"mesh.inp", i + 1}});
model.materials = {{"unused-elastic", 210e9, .3, {}}};
model.thermal_materials = {{{"plate", 1, "1"}, "first", 2, 3, 4, {}},
{{"plate", 2, "2"}, "second", 10, 1, 1, {}}};
model.thermal_sections = {{{"plate", 1, "1"}, "first", 1, 0, {}},
{{"plate", 2, "2"}, "second", 2, 1, {}}};
model.thermal_elements = {
{{"plate", 9, "0009"}, {{1, 2, 5, 4}}, 0, {"mesh.inp", 20}},
{{"plate", 2, "0002"}, {{2, 3, 6, 5}}, 1, {"mesh.inp", 21}}};
model.thermal_step = fesa::ThermalStepDefinition{"heat", .3, .9, 3};
model.thermal_step->boundaries = {{2, 11, 100, {}}};
const double large = std::ldexp(1., 54);
model.thermal_step->loads = {{2, 11, large, {}},
{2, 11, 1, {}},
{2, 11, -large, {}},
{2, 11, 5, {}},
{1, 15, -7, {}}};
return model;
}
fesa::Matrix Dense(const fesa::SparseMatrix& sparse) {
fesa::Matrix dense{sparse.Rows(), sparse.Columns()};
for (std::size_t i = 0; i < sparse.Rows(); ++i)
for (std::size_t j = sparse.RowOffsets()[i]; j < sparse.RowOffsets()[i + 1];
++j)
dense(i, sparse.ColumnIndices()[j]) = sparse.Values()[j];
return dense;
}
// Closed-form unit-square polynomial integrals, assembled with hand-fixed
// six-node scatter; no production kernel/layout is used by the expectation.
struct ExpectedMatrices {
fesa::Matrix k{30, 30};
fesa::Matrix c{30, 30};
fesa::Matrix pk{30, 30};
fesa::Matrix pc{30, 30};
};
ExpectedMatrices Expected() {
ExpectedMatrices result;
auto& k = result.k;
auto& c = result.c;
constexpr int m[4][4] = {
{4, 2, 1, 2}, {2, 4, 2, 1}, {1, 2, 4, 2}, {2, 1, 2, 4}};
constexpr int s[4][4] = {
{4, -1, -2, -1}, {-1, 4, -1, -2}, {-2, -1, 4, -1}, {-1, -2, -1, 4}};
constexpr int z0[5][5] = {{4, 2, -1, 0, 0},
{2, 16, 2, 0, 0},
{-1, 2, 8, 2, -1},
{0, 0, 2, 16, 2},
{0, 0, -1, 2, 4}};
constexpr int z1[5][5] = {{7, -8, 1, 0, 0},
{-8, 16, -8, 0, 0},
{1, -8, 14, -8, 1},
{0, 0, -8, 16, -8},
{0, 0, 1, -8, 7}};
const int nodes[2][4] = {{0, 1, 4, 3}, {1, 2, 5, 4}};
for (std::size_t e = 0; e < 2; ++e) {
const double h = e == 0 ? 1. : 2.;
const double conductivity = e == 0 ? 2. : 10.;
const double capacity = e == 0 ? 12. : 1.;
for (std::size_t i = 0; i < 4; ++i)
for (std::size_t j = 0; j < 4; ++j)
for (std::size_t a = 0; a < 5; ++a)
for (std::size_t b = 0; b < 5; ++b) {
const auto p = 5 * nodes[e][i] + a;
const auto q = 5 * nodes[e][j] + b;
c(p, q) += capacity * h * m[i][j] * z0[a][b] / 2160.;
k(p, q) += conductivity * (h * s[i][j] * z0[a][b] / 360. +
m[i][j] * z1[a][b] / (54 * h));
const double sx[4] = {-1, 1, 1, -1};
const double sy[4] = {-1, -1, 1, 1};
const double ps = (sx[i] == sx[j] ? 1. / 3 : 1. / 6) +
(sy[i] == sy[j] ? 1. / 3 : 1. / 6);
result.pk(p, q) +=
conductivity * (h * ps * std::abs(z0[a][b]) / 60 +
std::abs(m[i][j] * z1[a][b] / (54 * h)));
result.pc(p, q) +=
std::abs(capacity * h * m[i][j] * z0[a][b] / 2160.);
}
}
return result;
}
void SameBits(const std::vector<double>& a, const std::vector<double>& b) {
ASSERT_EQ(a.size(), b.size());
EXPECT_EQ(std::memcmp(a.data(), b.data(), a.size() * sizeof(double)), 0);
}
} // namespace
TEST(ThermalAssembler,
HandAssemblyPreservesSharedStationsMaterialOffsetAndThicknessJump) {
auto domain = fesa::Domain::Create(Model());
ASSERT_TRUE(domain.HasValue());
const auto model = fesa::AnalysisModel::Create(domain.Value());
ASSERT_TRUE(model.HasValue());
const auto dofs = fesa::DofManager::CreateThermal(model.Value());
ASSERT_TRUE(dofs.HasValue());
fesa::SerialParallelFor serial;
const auto assembled =
fesa::AssembleThermal(model.Value(), dofs.Value(), serial);
ASSERT_TRUE(assembled.HasValue());
ASSERT_EQ(assembled.Value().Conductivity().Rows(), 30U);
const auto expected = Expected();
const auto k = Dense(assembled.Value().Conductivity());
const auto c = Dense(assembled.Value().Capacity());
const auto ek = Dense(assembled.Value().ConductivityError());
const auto ec = Dense(assembled.Value().CapacityError());
const double gamma =
4096 * std::ldexp(1., -53) / (1 - 4096 * std::ldexp(1., -53));
for (std::size_t i = 0; i < 30; ++i)
for (std::size_t j = 0; j < 30; ++j) {
EXPECT_LE(std::abs(k(i, j) - expected.k(i, j)),
ek(i, j) + gamma * expected.pk(i, j));
EXPECT_LE(std::abs(c(i, j) - expected.c(i, j)),
ec(i, j) + gamma * expected.pc(i, j));
}
EXPECT_EQ(domain.Value().ThermalElements()[0].MaterialIndex(), 1U);
EXPECT_EQ(domain.Value().ThermalElements()[1].MaterialIndex(), 2U);
EXPECT_EQ(assembled.Value().ElementOrder(),
(std::vector<fesa::EntityIndex>{0, 1}));
EXPECT_EQ(assembled.Value().Kernels()[0].SourceId().source_label_text,
"0009");
for (std::size_t a = 0; a < 5; ++a)
EXPECT_EQ(assembled.Value().Scatters()[0][5 + a],
assembled.Value().Scatters()[1][a]);
}
TEST(ThermalAssembler,
SourceRowHeatInputRetainsPrescribedSignedDuplicateContributions) {
auto domain = fesa::Domain::Create(Model());
ASSERT_TRUE(domain.HasValue());
const auto model = fesa::AnalysisModel::Create(domain.Value());
ASSERT_TRUE(model.HasValue());
const auto dofs = fesa::DofManager::CreateThermal(model.Value());
ASSERT_TRUE(dofs.HasValue());
const auto result = fesa::AssembleThermal(model.Value(), dofs.Value(),
fesa::SerialParallelFor{});
ASSERT_TRUE(result.HasValue());
const auto full =
dofs.Value().FullThermalDof(2, fesa::ThermalDofComponent::kBottom);
EXPECT_FALSE(dofs.Value().FreeEquation(full).has_value());
EXPECT_EQ(result.Value().HeatInput()[full], 5.);
EXPECT_EQ(result.Value().HeatInput()[4], -7.);
EXPECT_GE(result.Value().HeatInputError()[full], 1.);
for (std::size_t i = 0; i < 30; ++i)
if (i != full && i != 4) {
EXPECT_EQ(result.Value().HeatInput()[i], 0.);
EXPECT_EQ(result.Value().HeatInputError()[i], 0.);
}
auto reordered = Model();
std::swap(reordered.thermal_step->loads[1], reordered.thermal_step->loads[2]);
auto second_domain = fesa::Domain::Create(std::move(reordered));
ASSERT_TRUE(second_domain.HasValue());
auto second_model = fesa::AnalysisModel::Create(second_domain.Value());
ASSERT_TRUE(second_model.HasValue());
auto second_dofs = fesa::DofManager::CreateThermal(second_model.Value());
ASSERT_TRUE(second_dofs.HasValue());
auto second = fesa::AssembleThermal(second_model.Value(), second_dofs.Value(),
fesa::SerialParallelFor{});
ASSERT_TRUE(second.HasValue());
EXPECT_EQ(second.Value().HeatInput()[full], 6.);
}
TEST(ThermalAssembler,
SerialAndActualTbbOperatorsBoundsAndFluxAreBitwiseEqual) {
auto domain = fesa::Domain::Create(Model());
ASSERT_TRUE(domain.HasValue());
const auto model = fesa::AnalysisModel::Create(domain.Value());
ASSERT_TRUE(model.HasValue());
const auto dofs = fesa::DofManager::CreateThermal(model.Value());
ASSERT_TRUE(dofs.HasValue());
const auto serial = fesa::AssembleThermal(model.Value(), dofs.Value(),
fesa::SerialParallelFor{});
const auto parallel = fesa::AssembleThermal(model.Value(), dofs.Value(),
fesa::TbbParallelFor{});
ASSERT_TRUE(serial.HasValue());
ASSERT_TRUE(parallel.HasValue());
EXPECT_EQ(serial.Value().Conductivity().RowOffsets(),
parallel.Value().Conductivity().RowOffsets());
EXPECT_EQ(serial.Value().Conductivity().ColumnIndices(),
parallel.Value().Conductivity().ColumnIndices());
SameBits(serial.Value().Conductivity().Values(),
parallel.Value().Conductivity().Values());
SameBits(serial.Value().Capacity().Values(),
parallel.Value().Capacity().Values());
SameBits(serial.Value().ConductivityError().Values(),
parallel.Value().ConductivityError().Values());
SameBits(serial.Value().CapacityError().Values(),
parallel.Value().CapacityError().Values());
EXPECT_EQ(
std::memcmp(serial.Value().HeatInput().Data(),
parallel.Value().HeatInput().Data(), 30 * sizeof(double)),
0);
fesa::Vector temperature{30};
for (std::size_t i = 0; i < 30; ++i)
temperature[i] = static_cast<double>(i) - 10;
const auto sf =
serial.Value().RecoverFlux(temperature, fesa::SerialParallelFor{});
const auto pf =
parallel.Value().RecoverFlux(temperature, fesa::TbbParallelFor{});
ASSERT_TRUE(sf.HasValue());
ASSERT_TRUE(pf.HasValue());
for (std::size_t e = 0; e < 2; ++e)
for (std::size_t r = 0; r < 24; ++r)
EXPECT_EQ(
std::memcmp(sf.Value()[e][r].Components().data(),
pf.Value()[e][r].Components().data(), 3 * sizeof(double)),
0);
}
TEST(ThermalAssembler, DisconnectedComponentsHaveNoCrossBlockEntries) {
auto definition = Model();
definition.thermal_step->loads.clear();
definition.thermal_step->boundaries.clear();
definition.thermal_elements[1].node_indices = {{7, 8, 9, 10}};
for (const auto point : std::array<std::array<double, 3>, 4>{
{{3, 0, 0}, {4, 0, 0}, {4, 1, 0}, {3, 1, 0}}}) {
const auto id = static_cast<std::int64_t>(definition.nodes.size() + 100);
definition.nodes.push_back({{"other", id, std::to_string(id)}, point, {}});
}
definition.thermal_elements[1].source_id.instance_name = "other";
auto domain = fesa::Domain::Create(std::move(definition));
ASSERT_TRUE(domain.HasValue());
auto model = fesa::AnalysisModel::Create(domain.Value());
ASSERT_TRUE(model.HasValue());
auto dofs = fesa::DofManager::CreateThermal(model.Value());
ASSERT_TRUE(dofs.HasValue());
auto result = fesa::AssembleThermal(model.Value(), dofs.Value(),
fesa::TbbParallelFor{});
ASSERT_TRUE(result.HasValue());
const auto& matrix = result.Value().Capacity();
ASSERT_EQ(matrix.Rows(), 40U);
for (std::size_t row = 0; row < 40; ++row)
for (std::size_t p = matrix.RowOffsets()[row];
p < matrix.RowOffsets()[row + 1]; ++p)
EXPECT_EQ(row < 20, matrix.ColumnIndices()[p] < 20);
}
TEST(ThermalAssembler, RejectsWrongProfilesPartitionsAndNonfiniteAccumulation) {
auto domain = fesa::Domain::Create(Model());
ASSERT_TRUE(domain.HasValue());
auto model = fesa::AnalysisModel::Create(domain.Value());
ASSERT_TRUE(model.HasValue());
EXPECT_FALSE(fesa::AssembleThermal(model.Value(), fesa::DofManager{},
fesa::SerialParallelFor{})
.HasValue());
auto other_definition = Model();
other_definition.thermal_step->boundaries.clear();
auto other_domain = fesa::Domain::Create(other_definition);
ASSERT_TRUE(other_domain.HasValue());
auto other_model = fesa::AnalysisModel::Create(other_domain.Value());
ASSERT_TRUE(other_model.HasValue());
auto other_dofs = fesa::DofManager::CreateThermal(other_model.Value());
ASSERT_TRUE(other_dofs.HasValue());
EXPECT_FALSE(fesa::AssembleThermal(model.Value(), other_dofs.Value(),
fesa::SerialParallelFor{})
.HasValue());
auto definition = Model();
definition.thermal_step->loads = {
{2, 11, std::numeric_limits<double>::max(), {}},
{2, 11, std::numeric_limits<double>::max(), {}}};
auto overflow_domain = fesa::Domain::Create(definition);
ASSERT_TRUE(overflow_domain.HasValue());
auto overflow_model = fesa::AnalysisModel::Create(overflow_domain.Value());
ASSERT_TRUE(overflow_model.HasValue());
auto overflow_dofs = fesa::DofManager::CreateThermal(overflow_model.Value());
ASSERT_TRUE(overflow_dofs.HasValue());
EXPECT_FALSE(fesa::AssembleThermal(overflow_model.Value(),
overflow_dofs.Value(),
fesa::TbbParallelFor{})
.HasValue());
}
TEST(ThermalAssembler, ParallelKernelFailuresSelectStableSourceElement) {
auto definition = Model();
for (auto& material : definition.thermal_materials) {
material.density = std::numeric_limits<double>::max();
material.specific_heat = 2;
}
auto domain = fesa::Domain::Create(definition);
ASSERT_TRUE(domain.HasValue());
auto model = fesa::AnalysisModel::Create(domain.Value());
ASSERT_TRUE(model.HasValue());
auto dofs = fesa::DofManager::CreateThermal(model.Value());
ASSERT_TRUE(dofs.HasValue());
const auto serial = fesa::AssembleThermal(model.Value(), dofs.Value(),
fesa::SerialParallelFor{});
const auto parallel = fesa::AssembleThermal(model.Value(), dofs.Value(),
fesa::TbbParallelFor{});
ASSERT_FALSE(serial.HasValue());
ASSERT_FALSE(parallel.HasValue());
EXPECT_EQ(serial.GetStatus().Diagnostics()[0].entity_identity, "plate:0009");
EXPECT_EQ(parallel.GetStatus().Diagnostics()[0].entity_identity,
"plate:0009");
}