feat: integrate reviewed thermal application dispatch

This commit is contained in:
KOKO\Mimi
2026-09-29 22:24:44 +09:00
4 changed files with 546 additions and 7 deletions
+12 -3
View File
@@ -9,6 +9,7 @@
#include "fesa/analysis/dynamic_analysis_state.h"
#include "fesa/analysis/eigen_analysis_state.h"
#include "fesa/analysis/nonlinear_analysis_state.h"
#include "fesa/analysis/transient_heat_procedure.h"
#include "fesa/core/diagnostic.h"
#include "fesa/core/status.h"
#include "fesa/model/domain.h"
@@ -24,6 +25,7 @@ class NonlinearStaticProcedureExecutor;
class ResultsWriter;
class HhtDynamicProcedureExecutor;
class DynamicResultsWriter;
class ThermalResultsWriter;
struct DerivedOutputPaths;
/// @brief Owns one parsed and semantically mapped analysis input.
@@ -60,27 +62,34 @@ struct DynamicExecutionResult {
using PreparedAnalysisOutcome =
std::variant<LinearStaticExecutionResult, NonlinearStaticExecutionResult,
EigenAnalysisState, DynamicExecutionResult>;
EigenAnalysisState, DynamicExecutionResult,
ThermalExecutionResult>;
/// @brief Selects the approved procedure from a prepared Domain.
/// @pre The executors and log outlive this call.
/// @param thermal_executor Required only for a thermal Domain, whose structural
/// step collection is empty; absence returns a solver failure.
/// @return A validated procedure outcome without opening a result file.
Result<PreparedAnalysisOutcome> ExecutePreparedAnalysis(
const Domain& domain, LinearStaticProcedureExecutor& linear_executor,
NonlinearStaticProcedureExecutor& nonlinear_executor,
const LinearFrequencyProcedureExecutor& frequency_executor,
NewtonIterationLogWriter& log,
const HhtDynamicProcedureExecutor* dynamic_executor = nullptr);
const HhtDynamicProcedureExecutor* dynamic_executor = nullptr,
const TransientHeatProcedureExecutor* thermal_executor = nullptr);
/// @brief Writes an already computed outcome to its authoritative result file.
/// @pre The Newton log has closed successfully before this function is called.
/// @param thermal_writer Required only for a thermal outcome; absence returns
/// an output failure without opening the final result.
/// @return Success after the selected writer atomically finalizes its result.
Status FinalizePreparedAnalysisResult(
const DerivedOutputPaths& paths, const Domain& domain,
const PreparedAnalysisOutcome& outcome,
const std::vector<Diagnostic>& diagnostics, ResultsWriter& linear_writer,
NonlinearResultsWriter& nonlinear_writer, EigenResultsWriter& eigen_writer,
DynamicResultsWriter* dynamic_writer = nullptr);
DynamicResultsWriter* dynamic_writer = nullptr,
ThermalResultsWriter* thermal_writer = nullptr);
} // namespace fesa
+37 -2
View File
@@ -14,6 +14,7 @@
#include "fesa/results/eigen_results_writer.h"
#include "fesa/results/nonlinear_results_writer.h"
#include "fesa/results/results_writer.h"
#include "fesa/results/thermal_results_writer.h"
namespace fesa {
@@ -40,7 +41,26 @@ Result<PreparedAnalysisOutcome> ExecutePreparedAnalysis(
NonlinearStaticProcedureExecutor& nonlinear_executor,
const LinearFrequencyProcedureExecutor& frequency_executor,
NewtonIterationLogWriter& log,
const HhtDynamicProcedureExecutor* dynamic_executor) {
const HhtDynamicProcedureExecutor* dynamic_executor,
const TransientHeatProcedureExecutor* thermal_executor) {
if (domain.ThermalStep() != nullptr) {
if (thermal_executor == nullptr) {
return Result<PreparedAnalysisOutcome>::Failure(
Status::Failure(FailureCategory::kSolver,
{{Severity::kError,
"missing-thermal-executor",
{},
"HEAT TRANSFER",
{},
"Thermal execution adapter is required."}}));
}
auto thermal = thermal_executor->Execute(domain);
if (!thermal.HasValue()) {
return Result<PreparedAnalysisOutcome>::Failure(thermal.GetStatus());
}
return Result<PreparedAnalysisOutcome>::Success(
PreparedAnalysisOutcome{std::move(thermal.Value())});
}
if (domain.Steps().Size() != 1U) {
return Result<PreparedAnalysisOutcome>::Failure(Status::Failure(
FailureCategory::kInput,
@@ -100,7 +120,22 @@ Status FinalizePreparedAnalysisResult(
const PreparedAnalysisOutcome& outcome,
const std::vector<Diagnostic>& diagnostics, ResultsWriter& linear_writer,
NonlinearResultsWriter& nonlinear_writer, EigenResultsWriter& eigen_writer,
DynamicResultsWriter* dynamic_writer) {
DynamicResultsWriter* dynamic_writer,
ThermalResultsWriter* thermal_writer) {
if (std::holds_alternative<ThermalExecutionResult>(outcome)) {
if (thermal_writer == nullptr) {
return Status::Failure(FailureCategory::kOutput,
{{Severity::kError,
"missing-thermal-writer",
{},
"HEAT TRANSFER",
{},
"Thermal result writer is required."}});
}
return thermal_writer->Write(
paths.results_path, domain,
std::get<ThermalExecutionResult>(outcome).state, diagnostics);
}
if (std::holds_alternative<DynamicExecutionResult>(outcome)) {
if (dynamic_writer == nullptr)
return Status::Failure(FailureCategory::kOutput,
+6 -2
View File
@@ -18,6 +18,7 @@
#include "fesa/io/hdf5/eigen_hdf5_results_writer.h"
#include "fesa/io/hdf5/hdf5_results_writer.h"
#include "fesa/io/hdf5/nonlinear_hdf5_results_writer.h"
#include "fesa/io/hdf5/thermal_hdf5_results_writer.h"
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
#include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h"
@@ -87,6 +88,7 @@ int FesaApplication::Run(const std::vector<std::string>& arguments) {
NonlinearHdf5ResultsWriter nonlinear_writer;
EigenHdf5ResultsWriter eigen_writer;
DynamicHdf5ResultsWriter dynamic_writer;
ThermalHdf5ResultsWriter thermal_writer;
NewtonIterationLogWriter log;
Status status = log.Open(paths.Value().log_path);
@@ -106,9 +108,11 @@ int FesaApplication::Run(const std::vector<std::string>& arguments) {
const LinearFrequencyProcedureExecutor frequency_executor;
const HhtDynamicProcedureExecutor dynamic_executor(
parallel_for, linear_solver, tangent_solver);
const TransientHeatProcedureExecutor thermal_executor(parallel_for,
linear_solver);
Result<PreparedAnalysisOutcome> outcome = ExecutePreparedAnalysis(
prepared.Value().domain, linear_executor, nonlinear_executor,
frequency_executor, log, &dynamic_executor);
frequency_executor, log, &dynamic_executor, &thermal_executor);
if (!outcome.HasValue()) {
status = outcome.GetStatus();
} else {
@@ -120,7 +124,7 @@ int FesaApplication::Run(const std::vector<std::string>& arguments) {
status = FinalizePreparedAnalysisResult(
paths.Value(), prepared.Value().domain, outcome.Value(),
prepared.Value().diagnostics, linear_writer, nonlinear_writer,
eigen_writer, &dynamic_writer);
eigen_writer, &dynamic_writer, &thermal_writer);
if (status.IsOk()) {
return kSuccessExitCode;
}
@@ -0,0 +1,491 @@
#include <gtest/gtest.h>
#include <hdf5.h>
#include <atomic>
#include <cmath>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <random>
#include <stdexcept>
#include <string>
#include <vector>
#include "fesa/app/fesa_application.h"
#define NOMINMAX
#include <Windows.h>
#include "fesa/analysis/linear_frequency_procedure.h"
#include "fesa/analysis/linear_static_procedure.h"
#include "fesa/analysis/newton_iteration_log.h"
#include "fesa/analysis/nonlinear_static_procedure.h"
#include "fesa/analysis/prepared_analysis.h"
#include "fesa/app/output_paths.h"
#include "fesa/assembly/parallel_for.h"
#include "fesa/io/hdf5/eigen_hdf5_results_writer.h"
#include "fesa/io/hdf5/hdf5_results_writer.h"
#include "fesa/io/hdf5/nonlinear_hdf5_results_writer.h"
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
#include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h"
namespace {
class HdfHandle {
public:
HdfHandle(hid_t value, herr_t (*close)(hid_t))
: value_(value), close_(close) {
if (value < 0) throw std::runtime_error{"Cannot open HDF test object."};
}
~HdfHandle() { close_(value_); }
HdfHandle(const HdfHandle&) = delete;
HdfHandle& operator=(const HdfHandle&) = delete;
hid_t Get() const { return value_; }
private:
hid_t value_;
herr_t (*close_)(hid_t);
};
std::vector<hsize_t> Shape(hid_t file, const std::string& path) {
HdfHandle data{H5Dopen2(file, path.c_str(), H5P_DEFAULT), H5Dclose};
HdfHandle space{H5Dget_space(data.Get()), H5Sclose};
const int rank = H5Sget_simple_extent_ndims(space.Get());
if (rank < 0) throw std::runtime_error{"Invalid HDF test shape."};
std::vector<hsize_t> shape(static_cast<std::size_t>(rank));
EXPECT_GE(H5Sget_simple_extent_dims(space.Get(), shape.data(), nullptr), 0);
return shape;
}
std::vector<double> Doubles(hid_t file, const std::string& path) {
std::size_t count = 1;
for (auto extent : Shape(file, path))
count *= static_cast<std::size_t>(extent);
std::vector<double> result(count);
HdfHandle data{H5Dopen2(file, path.c_str(), H5P_DEFAULT), H5Dclose};
EXPECT_GE(H5Dread(data.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL,
H5P_DEFAULT, result.data()),
0);
return result;
}
std::string StringAttribute(hid_t object, const char* name) {
HdfHandle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose};
HdfHandle type{H5Aget_type(attribute.Get()), H5Tclose};
char* value = nullptr;
EXPECT_GE(H5Aread(attribute.Get(), type.Get(), &value), 0);
const std::string result = value == nullptr ? "" : value;
if (value != nullptr) H5free_memory(value);
return result;
}
struct FrameRow {
std::uint64_t frame_index;
std::uint64_t increment_index;
double time;
double time_increment;
std::uint8_t is_initial;
};
std::vector<FrameRow> Frames(hid_t file) {
const std::string path = "/steps/Step-1/accepted_frames";
const auto shape = Shape(file, path);
if (shape.size() != 1) throw std::runtime_error{"Invalid frame shape."};
std::vector<FrameRow> frames(static_cast<std::size_t>(shape[0]));
HdfHandle type{H5Tcreate(H5T_COMPOUND, sizeof(FrameRow)), H5Tclose};
EXPECT_GE(H5Tinsert(type.Get(), "frame_index", HOFFSET(FrameRow, frame_index),
H5T_NATIVE_UINT64),
0);
EXPECT_GE(H5Tinsert(type.Get(), "increment_index",
HOFFSET(FrameRow, increment_index), H5T_NATIVE_UINT64),
0);
EXPECT_GE(
H5Tinsert(type.Get(), "time", HOFFSET(FrameRow, time), H5T_NATIVE_DOUBLE),
0);
EXPECT_GE(H5Tinsert(type.Get(), "time_increment",
HOFFSET(FrameRow, time_increment), H5T_NATIVE_DOUBLE),
0);
EXPECT_GE(H5Tinsert(type.Get(), "is_initial", HOFFSET(FrameRow, is_initial),
H5T_NATIVE_UINT8),
0);
HdfHandle data{H5Dopen2(file, path.c_str(), H5P_DEFAULT), H5Dclose};
EXPECT_GE(H5Dread(data.Get(), type.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT,
frames.data()),
0);
return frames;
}
std::string ReadBytes(const std::filesystem::path& path) {
std::ifstream input{path, std::ios::binary};
return {std::istreambuf_iterator<char>{input},
std::istreambuf_iterator<char>{}};
}
std::string Replace(std::string text, const std::string& from,
const std::string& to) {
const auto position = text.find(from);
if (position == std::string::npos)
throw std::runtime_error{"Missing fixture text."};
text.replace(position, from.size(), to);
return text;
}
struct AppRun {
int exit_code;
std::string error;
};
AppRun RunApplication(const std::filesystem::path& input) {
testing::internal::CaptureStderr();
const int result = fesa::FesaApplication{}.Run({input.string()});
return {result, testing::internal::GetCapturedStderr()};
}
class ThermalApplicationTest : public testing::Test {
protected:
void SetUp() override {
static std::atomic<unsigned long> counter{0};
do {
directory_ =
std::filesystem::temp_directory_path() /
("fesa-thermal-app-" + std::to_string(std::random_device{}()) + "-" +
std::to_string(counter.fetch_add(1)));
} while (!std::filesystem::create_directory(directory_));
}
void TearDown() override {
std::error_code error;
std::filesystem::remove_all(directory_, error);
}
std::filesystem::path Path(const std::string& name) const {
return directory_ / name;
}
void Write(const std::string& name, const std::string& text) {
std::ofstream output{Path(name), std::ios::binary};
output << text;
ASSERT_TRUE(output.good());
}
std::filesystem::path directory_;
};
std::string Deck(const std::string& controls = ".3,.7",
const std::string& initial = "-2") {
return R"inp(*Part,name=P
*Node
1,0,0,0
2,1,0,0
3,1,1,0
4,0,1,0
*Element,type=DS4
1,1,2,3,4
*Elset,elset=E
1
*Shell Section,elset=E,material=M
1,5
*End Part
*Assembly,name=A
*Instance,name=I,part=P
*End Instance
*Nset,nset=All,instance=I
1,2,3,4
*End Assembly
*Material,name=M
*Density
1
*Conductivity
1
*Specific Heat
1
*Initial Conditions,type=TEMPERATURE
All,)inp" +
initial + R"inp(
*Step,name=Raw Heat,inc=10
*Heat Transfer
)inp" + controls +
R"inp(
*Output,field,variable=PRESELECT
*End Step
)inp";
}
TEST_F(ThermalApplicationTest, ExecutesThermalInputThroughAtomicOutput) {
Write("Heat.Model.INP", Deck());
const auto run = RunApplication(Path("Heat.Model.INP"));
ASSERT_EQ(run.exit_code, 0) << run.error;
EXPECT_GT(H5Fis_hdf5(Path("Heat.Model_results.h5").string().c_str()), 0);
EXPECT_EQ(std::filesystem::file_size(Path("Heat.Model_log.txt")), 0U);
HdfHandle file{H5Fopen(Path("Heat.Model_results.h5").string().c_str(),
H5F_ACC_RDONLY, H5P_DEFAULT),
H5Fclose};
HdfHandle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose};
EXPECT_EQ(StringAttribute(metadata.Get(), "source_step_name"), "Raw Heat");
EXPECT_EQ(StringAttribute(metadata.Get(), "feature_id"),
"shell-transient-heat-transfer");
EXPECT_EQ(StringAttribute(metadata.Get(), "analysis_procedure"),
"uncoupled-transient-heat-transfer");
EXPECT_EQ(StringAttribute(metadata.Get(), "source_input_identity")
.find("path=" + Path("Heat.Model.INP").generic_u8string() +
";content_identity="),
0U);
const auto frames = Frames(file.Get());
ASSERT_EQ(frames.size(), 4U);
for (std::size_t index = 0; index < frames.size(); ++index) {
const auto& frame = frames[index];
EXPECT_EQ(frame.frame_index, index);
EXPECT_EQ(frame.increment_index, index);
EXPECT_EQ(frame.is_initial, index == 0 ? 1 : 0);
EXPECT_DOUBLE_EQ(frame.time,
index == 3 ? .7 : static_cast<double>(index) * .3);
EXPECT_DOUBLE_EQ(frame.time_increment, index == 0 ? 0
: index == 3 ? std::fma(-2., .3, .7)
: .3);
const auto root = "/steps/Step-1/frames/" + std::to_string(index);
EXPECT_EQ(Shape(file.Get(), root + "/nodal/temperature"),
(std::vector<hsize_t>{4, 5}));
for (double value : Doubles(file.Get(), root + "/nodal/temperature")) {
EXPECT_TRUE(std::isfinite(value));
EXPECT_LT(value, 0.);
if (index == 0) EXPECT_DOUBLE_EQ(value, -2.);
}
if (index == 0) {
EXPECT_EQ(
H5Lexists(file.Get(), (root + "/nodal/reaction_heat_rate").c_str(),
H5P_DEFAULT),
0);
EXPECT_EQ(H5Lexists(file.Get(), (root + "/element").c_str(), H5P_DEFAULT),
0);
} else {
EXPECT_EQ(Shape(file.Get(), root + "/nodal/reaction_heat_rate"),
(std::vector<hsize_t>{0}));
EXPECT_EQ(Shape(file.Get(), root + "/element/heat_flux"),
(std::vector<hsize_t>{1, 4, 6, 3}));
}
}
EXPECT_EQ(Shape(file.Get(), "/diagnostics"), (std::vector<hsize_t>{1}));
HdfHandle diagnostics{H5Dopen2(file.Get(), "/diagnostics", H5P_DEFAULT),
H5Dclose};
HdfHandle string_type{H5Tcopy(H5T_C_S1), H5Tclose};
ASSERT_GE(H5Tset_size(string_type.Get(), H5T_VARIABLE), 0);
ASSERT_GE(H5Tset_cset(string_type.Get(), H5T_CSET_UTF8), 0);
HdfHandle row_type{H5Tcreate(H5T_COMPOUND, sizeof(char*)), H5Tclose};
ASSERT_GE(H5Tinsert(row_type.Get(), "code", 0, string_type.Get()), 0);
char* code = nullptr;
ASSERT_GE(H5Dread(diagnostics.Get(), row_type.Get(), H5S_ALL, H5S_ALL,
H5P_DEFAULT, &code),
0);
EXPECT_STREQ(code, "ignored-input-keyword");
H5free_memory(code);
}
TEST_F(ThermalApplicationTest,
EndpointCloseUsesActualDeltaAndLargeIncrementUsesPeriod) {
for (const auto& controls : {".3,.9", "2,.7"}) {
SCOPED_TRACE(controls);
Write("model.inp", Deck(controls, "0"));
const auto run = RunApplication(Path("model.inp"));
ASSERT_EQ(run.exit_code, 0) << run.error;
HdfHandle file{H5Fopen(Path("model_results.h5").string().c_str(),
H5F_ACC_RDONLY, H5P_DEFAULT),
H5Fclose};
const auto frames = Frames(file.Get());
if (std::string{controls} == ".3,.9") {
ASSERT_EQ(frames.size(), 4U);
EXPECT_DOUBLE_EQ(frames.back().time, .9);
EXPECT_DOUBLE_EQ(frames.back().time_increment, .3);
EXPECT_NE(frames.back().time_increment, frames[3].time - frames[2].time);
} else {
ASSERT_EQ(frames.size(), 2U);
EXPECT_DOUBLE_EQ(frames.back().time, .7);
EXPECT_DOUBLE_EQ(frames.back().time_increment, .7);
}
}
}
TEST_F(ThermalApplicationTest, RawInitialPrecedesPrescribedBoundaryProjection) {
auto deck = Replace(Deck(".1,.2"), "*Output,field",
"*Boundary\nAll,11,11,1\nAll,15,15,1\n*Output,field");
Write("model.inp", deck);
const auto run = RunApplication(Path("model.inp"));
ASSERT_EQ(run.exit_code, 0) << run.error;
HdfHandle file{H5Fopen(Path("model_results.h5").string().c_str(),
H5F_ACC_RDONLY, H5P_DEFAULT),
H5Fclose};
for (double value :
Doubles(file.Get(), "/steps/Step-1/frames/0/nodal/temperature"))
EXPECT_DOUBLE_EQ(value, -2);
for (int frame = 1; frame <= 2; ++frame) {
const auto root = "/steps/Step-1/frames/" + std::to_string(frame);
const auto temperature = Doubles(file.Get(), root + "/nodal/temperature");
for (std::size_t node = 0; node < 4; ++node) {
EXPECT_DOUBLE_EQ(temperature[5 * node], 1);
EXPECT_DOUBLE_EQ(temperature[5 * node + 4], 1);
}
EXPECT_EQ(Shape(file.Get(), root + "/nodal/reaction_heat_rate"),
(std::vector<hsize_t>{8}));
}
}
TEST_F(ThermalApplicationTest, InputAndSolverFailuresPreservePreviousFinal) {
const std::string previous{"previous final\0bytes", 20};
for (const auto& test : std::vector<std::pair<std::string, int>>{
{Replace(Deck(), "*Output,field",
"*Boundary\nAll,12,12,1\n*Output,field"),
3},
{Deck("1.e-300,1.e-300", "1.e200"), 5}}) {
Write("model.inp", test.first);
Write("model_results.h5", previous);
const auto run = RunApplication(Path("model.inp"));
EXPECT_EQ(run.exit_code, test.second) << run.error;
EXPECT_FALSE(run.error.empty());
EXPECT_EQ(ReadBytes(Path("model_results.h5")), previous);
EXPECT_EQ(std::filesystem::file_size(Path("model_log.txt")), 0U);
}
}
TEST_F(ThermalApplicationTest,
BusyLeasePrecedesParsingAndPreservesBothOutputs) {
Write("model.inp", "deliberately invalid input");
Write("model_results.h5", "previous final");
Write("model_log.txt", "previous log");
fesa::DerivedOutputPaths paths{Path("model.inp"), Path("model_results.h5"),
Path("model_log.txt")};
auto lease = fesa::AcquireOutputWriteLease(paths);
ASSERT_TRUE(lease.HasValue());
const auto run = RunApplication(paths.input_path);
EXPECT_EQ(run.exit_code, 6);
EXPECT_NE(run.error.find("code=output-busy"), std::string::npos);
EXPECT_EQ(ReadBytes(paths.results_path), "previous final");
EXPECT_EQ(ReadBytes(paths.log_path), "previous log");
}
TEST_F(ThermalApplicationTest,
RealFinalizeFailurePreservesFinalAndRemovesCandidate) {
Write("model.inp", Deck(".1,.1", "0"));
Write("model_results.h5", "previous final");
// Deny replacement of this final while permitting reads. The output lease is
// separate; the candidate must reach the actual atomic finalize operation.
const HANDLE locked =
CreateFileW(Path("model_results.h5").c_str(), GENERIC_READ,
FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
ASSERT_NE(locked, INVALID_HANDLE_VALUE);
const auto run = RunApplication(Path("model.inp"));
EXPECT_TRUE(CloseHandle(locked));
EXPECT_EQ(run.exit_code, 6) << run.error;
EXPECT_EQ(ReadBytes(Path("model_results.h5")), "previous final");
EXPECT_EQ(std::filesystem::file_size(Path("model_log.txt")), 0U);
for (const auto& entry : std::filesystem::directory_iterator{directory_}) {
EXPECT_EQ(entry.path().filename().string().find(".tmp."),
std::string::npos);
}
}
TEST_F(ThermalApplicationTest, MissingAdaptersHaveSpecificFailureCategories) {
Write("model.inp", Deck(".1,.1", "0"));
auto prepared = fesa::PrepareAnalysisInput(Path("model.inp"));
ASSERT_TRUE(prepared.HasValue());
ASSERT_EQ(prepared.Value().domain.Steps().Size(), 0U);
fesa::SerialParallelFor parallel;
fesa::MklPardisoSolver solver;
fesa::MklPardisoTangentSolver tangent;
fesa::LinearStaticProcedureExecutor linear{parallel, solver};
fesa::NonlinearStaticProcedureExecutor nonlinear{parallel, tangent};
fesa::LinearFrequencyProcedureExecutor frequency;
fesa::TransientHeatProcedureExecutor thermal{parallel, solver};
fesa::NewtonIterationLogWriter log;
ASSERT_TRUE(log.Open(Path("model_log.txt")).IsOk());
auto missing = fesa::ExecutePreparedAnalysis(prepared.Value().domain, linear,
nonlinear, frequency, log);
ASSERT_FALSE(missing.HasValue());
EXPECT_EQ(missing.GetStatus().Category(), fesa::FailureCategory::kSolver);
ASSERT_EQ(missing.GetStatus().Diagnostics().size(), 1U);
EXPECT_EQ(missing.GetStatus().Diagnostics()[0].code,
"missing-thermal-executor");
auto outcome =
fesa::ExecutePreparedAnalysis(prepared.Value().domain, linear, nonlinear,
frequency, log, nullptr, &thermal);
ASSERT_TRUE(outcome.HasValue());
ASSERT_TRUE(
std::holds_alternative<fesa::ThermalExecutionResult>(outcome.Value()));
ASSERT_TRUE(log.Close().IsOk());
fesa::Hdf5ResultsWriter linear_writer;
fesa::NonlinearHdf5ResultsWriter nonlinear_writer;
fesa::EigenHdf5ResultsWriter eigen_writer;
const fesa::DerivedOutputPaths paths{
Path("model.inp"), Path("model_results.h5"), Path("model_log.txt")};
Write("model_results.h5", "previous final");
const auto status = fesa::FinalizePreparedAnalysisResult(
paths, prepared.Value().domain, outcome.Value(),
prepared.Value().diagnostics, linear_writer, nonlinear_writer,
eigen_writer);
EXPECT_EQ(status.Category(), fesa::FailureCategory::kOutput);
ASSERT_EQ(status.Diagnostics().size(), 1U);
EXPECT_EQ(status.Diagnostics()[0].code, "missing-thermal-writer");
EXPECT_EQ(ReadBytes(paths.results_path), "previous final");
}
TEST_F(ThermalApplicationTest,
ExistingStructuralStaticNonlinearAndHhtRoutesRemainAvailable) {
auto base = Replace(Deck(), "type=DS4", "type=S4");
base = Replace(base, "*Conductivity\n1\n*Specific Heat\n1",
"*Elastic\n1000,.25");
base.erase(base.find("*Initial Conditions"));
base += "*Boundary\nAll,1,6\n";
for (const auto& step :
{"*Step,name=Structural,nlgeom=NO\n*Static\n.1,.1,.01,.1\n*End Step\n",
"*Step,name=Structural,nlgeom=YES\n*Static\n.1,1.,.01,.1\n*End Step\n",
"*Step,name=Structural,nlgeom=YES,inc=10\n*Dynamic,direct\n.1,.2\n*End "
"Step\n"}) {
SCOPED_TRACE(step);
const bool dynamic =
std::string{step}.find("*Dynamic") != std::string::npos;
Write("model.inp",
(dynamic ? base : Replace(base, "*Density\n1\n", "")) + step);
const auto run = RunApplication(Path("model.inp"));
ASSERT_EQ(run.exit_code, 0) << run.error;
EXPECT_GT(H5Fis_hdf5(Path("model_results.h5").string().c_str()), 0);
}
}
TEST_F(ThermalApplicationTest,
NativeExecutableAcceptsThermalInputAndRejectsInteriorBoundary) {
std::vector<wchar_t> module(32768);
const DWORD length = GetModuleFileNameW(nullptr, module.data(),
static_cast<DWORD>(module.size()));
ASSERT_GT(length, 0U);
ASSERT_LT(length, module.size());
const auto executable =
std::filesystem::path{std::wstring{module.data(), length}}
.parent_path()
.parent_path() /
"src/fesa/fesa.exe";
ASSERT_TRUE(std::filesystem::exists(executable));
std::string previous;
for (const bool valid : {true, false}) {
Write("native.inp", valid
? Deck(".1,.1", "0")
: Replace(Deck(), "*Output,field",
"*Boundary\nAll,12,12,1\n*Output,field"));
std::wstring command = L"\"" + executable.wstring() + L"\" \"" +
Path("native.inp").wstring() + L"\"";
STARTUPINFOW startup{};
startup.cb = sizeof(startup);
PROCESS_INFORMATION process{};
ASSERT_TRUE(CreateProcessW(executable.c_str(), command.data(), nullptr,
nullptr, FALSE, CREATE_NO_WINDOW, nullptr,
nullptr, &startup, &process));
const DWORD wait = WaitForSingleObject(process.hProcess, 60000);
if (wait != WAIT_OBJECT_0) TerminateProcess(process.hProcess, 99);
DWORD exit = 99;
EXPECT_TRUE(GetExitCodeProcess(process.hProcess, &exit));
CloseHandle(process.hThread);
CloseHandle(process.hProcess);
ASSERT_EQ(wait, WAIT_OBJECT_0);
EXPECT_EQ(exit, valid ? 0U : 3U);
EXPECT_GT(H5Fis_hdf5(Path("native_results.h5").string().c_str()), 0);
if (valid) {
previous = ReadBytes(Path("native_results.h5"));
} else {
EXPECT_EQ(ReadBytes(Path("native_results.h5")), previous);
}
}
}
} // namespace