feat: integrate reviewed thermal application dispatch
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
#include "fesa/analysis/dynamic_analysis_state.h"
|
#include "fesa/analysis/dynamic_analysis_state.h"
|
||||||
#include "fesa/analysis/eigen_analysis_state.h"
|
#include "fesa/analysis/eigen_analysis_state.h"
|
||||||
#include "fesa/analysis/nonlinear_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/diagnostic.h"
|
||||||
#include "fesa/core/status.h"
|
#include "fesa/core/status.h"
|
||||||
#include "fesa/model/domain.h"
|
#include "fesa/model/domain.h"
|
||||||
@@ -24,6 +25,7 @@ class NonlinearStaticProcedureExecutor;
|
|||||||
class ResultsWriter;
|
class ResultsWriter;
|
||||||
class HhtDynamicProcedureExecutor;
|
class HhtDynamicProcedureExecutor;
|
||||||
class DynamicResultsWriter;
|
class DynamicResultsWriter;
|
||||||
|
class ThermalResultsWriter;
|
||||||
struct DerivedOutputPaths;
|
struct DerivedOutputPaths;
|
||||||
|
|
||||||
/// @brief Owns one parsed and semantically mapped analysis input.
|
/// @brief Owns one parsed and semantically mapped analysis input.
|
||||||
@@ -60,27 +62,34 @@ struct DynamicExecutionResult {
|
|||||||
|
|
||||||
using PreparedAnalysisOutcome =
|
using PreparedAnalysisOutcome =
|
||||||
std::variant<LinearStaticExecutionResult, NonlinearStaticExecutionResult,
|
std::variant<LinearStaticExecutionResult, NonlinearStaticExecutionResult,
|
||||||
EigenAnalysisState, DynamicExecutionResult>;
|
EigenAnalysisState, DynamicExecutionResult,
|
||||||
|
ThermalExecutionResult>;
|
||||||
|
|
||||||
/// @brief Selects the approved procedure from a prepared Domain.
|
/// @brief Selects the approved procedure from a prepared Domain.
|
||||||
/// @pre The executors and log outlive this call.
|
/// @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.
|
/// @return A validated procedure outcome without opening a result file.
|
||||||
Result<PreparedAnalysisOutcome> ExecutePreparedAnalysis(
|
Result<PreparedAnalysisOutcome> ExecutePreparedAnalysis(
|
||||||
const Domain& domain, LinearStaticProcedureExecutor& linear_executor,
|
const Domain& domain, LinearStaticProcedureExecutor& linear_executor,
|
||||||
NonlinearStaticProcedureExecutor& nonlinear_executor,
|
NonlinearStaticProcedureExecutor& nonlinear_executor,
|
||||||
const LinearFrequencyProcedureExecutor& frequency_executor,
|
const LinearFrequencyProcedureExecutor& frequency_executor,
|
||||||
NewtonIterationLogWriter& log,
|
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.
|
/// @brief Writes an already computed outcome to its authoritative result file.
|
||||||
/// @pre The Newton log has closed successfully before this function is called.
|
/// @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.
|
/// @return Success after the selected writer atomically finalizes its result.
|
||||||
Status FinalizePreparedAnalysisResult(
|
Status FinalizePreparedAnalysisResult(
|
||||||
const DerivedOutputPaths& paths, const Domain& domain,
|
const DerivedOutputPaths& paths, const Domain& domain,
|
||||||
const PreparedAnalysisOutcome& outcome,
|
const PreparedAnalysisOutcome& outcome,
|
||||||
const std::vector<Diagnostic>& diagnostics, ResultsWriter& linear_writer,
|
const std::vector<Diagnostic>& diagnostics, ResultsWriter& linear_writer,
|
||||||
NonlinearResultsWriter& nonlinear_writer, EigenResultsWriter& eigen_writer,
|
NonlinearResultsWriter& nonlinear_writer, EigenResultsWriter& eigen_writer,
|
||||||
DynamicResultsWriter* dynamic_writer = nullptr);
|
DynamicResultsWriter* dynamic_writer = nullptr,
|
||||||
|
ThermalResultsWriter* thermal_writer = nullptr);
|
||||||
|
|
||||||
} // namespace fesa
|
} // namespace fesa
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include "fesa/results/eigen_results_writer.h"
|
#include "fesa/results/eigen_results_writer.h"
|
||||||
#include "fesa/results/nonlinear_results_writer.h"
|
#include "fesa/results/nonlinear_results_writer.h"
|
||||||
#include "fesa/results/results_writer.h"
|
#include "fesa/results/results_writer.h"
|
||||||
|
#include "fesa/results/thermal_results_writer.h"
|
||||||
|
|
||||||
namespace fesa {
|
namespace fesa {
|
||||||
|
|
||||||
@@ -40,7 +41,26 @@ Result<PreparedAnalysisOutcome> ExecutePreparedAnalysis(
|
|||||||
NonlinearStaticProcedureExecutor& nonlinear_executor,
|
NonlinearStaticProcedureExecutor& nonlinear_executor,
|
||||||
const LinearFrequencyProcedureExecutor& frequency_executor,
|
const LinearFrequencyProcedureExecutor& frequency_executor,
|
||||||
NewtonIterationLogWriter& log,
|
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) {
|
if (domain.Steps().Size() != 1U) {
|
||||||
return Result<PreparedAnalysisOutcome>::Failure(Status::Failure(
|
return Result<PreparedAnalysisOutcome>::Failure(Status::Failure(
|
||||||
FailureCategory::kInput,
|
FailureCategory::kInput,
|
||||||
@@ -100,7 +120,22 @@ Status FinalizePreparedAnalysisResult(
|
|||||||
const PreparedAnalysisOutcome& outcome,
|
const PreparedAnalysisOutcome& outcome,
|
||||||
const std::vector<Diagnostic>& diagnostics, ResultsWriter& linear_writer,
|
const std::vector<Diagnostic>& diagnostics, ResultsWriter& linear_writer,
|
||||||
NonlinearResultsWriter& nonlinear_writer, EigenResultsWriter& eigen_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 (std::holds_alternative<DynamicExecutionResult>(outcome)) {
|
||||||
if (dynamic_writer == nullptr)
|
if (dynamic_writer == nullptr)
|
||||||
return Status::Failure(FailureCategory::kOutput,
|
return Status::Failure(FailureCategory::kOutput,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include "fesa/io/hdf5/eigen_hdf5_results_writer.h"
|
#include "fesa/io/hdf5/eigen_hdf5_results_writer.h"
|
||||||
#include "fesa/io/hdf5/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/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_solver.h"
|
||||||
#include "fesa/solvers/linear/mkl_pardiso_tangent_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;
|
NonlinearHdf5ResultsWriter nonlinear_writer;
|
||||||
EigenHdf5ResultsWriter eigen_writer;
|
EigenHdf5ResultsWriter eigen_writer;
|
||||||
DynamicHdf5ResultsWriter dynamic_writer;
|
DynamicHdf5ResultsWriter dynamic_writer;
|
||||||
|
ThermalHdf5ResultsWriter thermal_writer;
|
||||||
NewtonIterationLogWriter log;
|
NewtonIterationLogWriter log;
|
||||||
|
|
||||||
Status status = log.Open(paths.Value().log_path);
|
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 LinearFrequencyProcedureExecutor frequency_executor;
|
||||||
const HhtDynamicProcedureExecutor dynamic_executor(
|
const HhtDynamicProcedureExecutor dynamic_executor(
|
||||||
parallel_for, linear_solver, tangent_solver);
|
parallel_for, linear_solver, tangent_solver);
|
||||||
|
const TransientHeatProcedureExecutor thermal_executor(parallel_for,
|
||||||
|
linear_solver);
|
||||||
Result<PreparedAnalysisOutcome> outcome = ExecutePreparedAnalysis(
|
Result<PreparedAnalysisOutcome> outcome = ExecutePreparedAnalysis(
|
||||||
prepared.Value().domain, linear_executor, nonlinear_executor,
|
prepared.Value().domain, linear_executor, nonlinear_executor,
|
||||||
frequency_executor, log, &dynamic_executor);
|
frequency_executor, log, &dynamic_executor, &thermal_executor);
|
||||||
if (!outcome.HasValue()) {
|
if (!outcome.HasValue()) {
|
||||||
status = outcome.GetStatus();
|
status = outcome.GetStatus();
|
||||||
} else {
|
} else {
|
||||||
@@ -120,7 +124,7 @@ int FesaApplication::Run(const std::vector<std::string>& arguments) {
|
|||||||
status = FinalizePreparedAnalysisResult(
|
status = FinalizePreparedAnalysisResult(
|
||||||
paths.Value(), prepared.Value().domain, outcome.Value(),
|
paths.Value(), prepared.Value().domain, outcome.Value(),
|
||||||
prepared.Value().diagnostics, linear_writer, nonlinear_writer,
|
prepared.Value().diagnostics, linear_writer, nonlinear_writer,
|
||||||
eigen_writer, &dynamic_writer);
|
eigen_writer, &dynamic_writer, &thermal_writer);
|
||||||
if (status.IsOk()) {
|
if (status.IsOk()) {
|
||||||
return kSuccessExitCode;
|
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
|
||||||
Reference in New Issue
Block a user