diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..45d3cc7 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(head:*)", + "Bash(tail:*)" + ] + } +} diff --git a/include/exodusIIcpp/enums.h b/include/exodusIIcpp/enums.h index aeb8731..507a147 100644 --- a/include/exodusIIcpp/enums.h +++ b/include/exodusIIcpp/enums.h @@ -8,7 +8,8 @@ namespace exodusIIcpp { /* clang-format off */ enum class FileAccess { READ, - WRITE + WRITE, + APPEND }; /* clang-format on */ diff --git a/include/exodusIIcpp/file.h b/include/exodusIIcpp/file.h index 12333b1..ff92fa7 100644 --- a/include/exodusIIcpp/file.h +++ b/include/exodusIIcpp/file.h @@ -66,7 +66,8 @@ class File { /// @param file_path Path to the file to open/create /// @param file_access Desired file access /// - ``exodusIIcpp::FileAccess::READ`` for reading, - /// - ``exodusIIcpp::FileAccess::WRITE`` for writing. + /// - ``exodusIIcpp::FileAccess::WRITE`` for writing, + /// - ``exodusIIcpp::FileAccess::APPEND`` for appending to an existing file. explicit File(exodusIIcpp::fs::path file_path, exodusIIcpp::FileAccess file_access); ~File(); @@ -80,6 +81,11 @@ class File { /// @param file_path Path to the file to create void create(const std::string & file_path); + /// Open an existing ExodusII file for appending new time steps + /// + /// @param file_path Path to the file to open + void append(const std::string & file_path); + /// Is file opened /// /// @return `true` if opened, `false` otherwise diff --git a/python/src/exodusIIcpp.cpp b/python/src/exodusIIcpp.cpp index e3cafb9..c486c14 100644 --- a/python/src/exodusIIcpp.cpp +++ b/python/src/exodusIIcpp.cpp @@ -19,6 +19,7 @@ PYBIND11_MODULE(exodusIIcpp, m) .def(py::init()) .def("open", &File::open) .def("create", &File::create) + .def("append", &File::append) .def("is_opened", &File::is_opened) .def("init", static_cast(&File::init)) .def("init", diff --git a/src/file.cpp b/src/file.cpp index 722c92a..03e9082 100644 --- a/src/file.cpp +++ b/src/file.cpp @@ -105,6 +105,10 @@ File::File(exodusIIcpp::fs::path file_path, exodusIIcpp::FileAccess file_access) open(file_path.string()); init(); } + else if (file_access == exodusIIcpp::FileAccess::APPEND) { + append(file_path.string()); + init(); + } else create(file_path.string()); } @@ -137,6 +141,19 @@ File::create(const std::string & file_path) throw Exception(fmt::sprintf("Unable to open file '%s'.", file_path)); } +void +File::append(const std::string & file_path) +{ + this->file_access = exodusIIcpp::FileAccess::APPEND; + this->exoid = ex_open(file_path.c_str(), + EX_WRITE, + &this->cpu_word_size, + &this->io_word_size, + &this->version); + if (this->exoid < 0) + throw Exception(fmt::sprintf("Unable to open file '%s'.", file_path)); +} + bool File::is_opened() const { @@ -146,7 +163,8 @@ File::is_opened() const void File::init() { - if (this->file_access == exodusIIcpp::FileAccess::READ) { + if (this->file_access == exodusIIcpp::FileAccess::READ || + this->file_access == exodusIIcpp::FileAccess::APPEND) { char title[MAX_LINE_LENGTH + 1]; memset(title, 0, sizeof(title)); EXODUSIICPP_CHECK_ERROR(ex_get_init(this->exoid, diff --git a/test/File_test.cpp b/test/File_test.cpp index b1095f2..aa269cb 100644 --- a/test/File_test.cpp +++ b/test/File_test.cpp @@ -188,6 +188,80 @@ TEST(FileTest, create_edge2) EXPECT_EQ(g.get_num_side_sets(), 0); } +TEST(FileTest, append_time_step) +{ + // Create initial file with one time step + { + File f(std::string("append_test.e"), FileAccess::WRITE); + EXPECT_TRUE(f.is_opened()); + f.init("test", 2, 3, 1, 1, 0, 0); + + std::vector x = { 0, 1, 0 }; + std::vector y = { 0, 0, 1 }; + f.write_coords(x, y); + f.write_coord_names(); + + std::vector connect1 = { 1, 2, 3 }; + f.write_block(1, "TRI3", 1, connect1); + std::vector blk_names = { "blk1" }; + f.write_block_names(blk_names); + + f.write_time(1, 0.0); + + std::vector nv_names = { "nv1" }; + f.write_nodal_var_names(nv_names); + f.write_nodal_var(1, 1, { 1.0, 2.0, 3.0 }); + + f.update(); + f.close(); + } + + // Verify initial file has 1 time step + { + File f(std::string("append_test.e"), FileAccess::READ); + EXPECT_TRUE(f.is_opened()); + f.read_times(); + EXPECT_EQ(f.get_num_times(), 1); + f.close(); + } + + // Open with APPEND and add another time step + { + File f(std::string("append_test.e"), FileAccess::APPEND); + EXPECT_TRUE(f.is_opened()); + f.read_times(); + EXPECT_EQ(f.get_num_times(), 1); + + // Write second time step + int next_step = f.get_num_times() + 1; + f.write_time(next_step, 1.0); + f.write_nodal_var(next_step, 1, { 2.0, 4.0, 6.0 }); + + f.update(); + f.close(); + } + + // Verify file now has 2 time steps + { + File f(std::string("append_test.e"), FileAccess::READ); + EXPECT_TRUE(f.is_opened()); + f.read_times(); + EXPECT_EQ(f.get_num_times(), 2); + + const std::vector & times = f.get_times(); + EXPECT_THAT(times, ElementsAre(DoubleEq(0.0), DoubleEq(1.0))); + + // Verify variable values for both time steps + auto ts1_values = f.get_nodal_variable_values(1, 1); + EXPECT_THAT(ts1_values, ElementsAre(DoubleEq(1.0), DoubleEq(2.0), DoubleEq(3.0))); + + auto ts2_values = f.get_nodal_variable_values(2, 1); + EXPECT_THAT(ts2_values, ElementsAre(DoubleEq(2.0), DoubleEq(4.0), DoubleEq(6.0))); + + f.close(); + } +} + TEST(FileTest, read_square) { File f(std::string(EXODUSIICPP_UNIT_TEST_ASSETS) + std::string("/square.e"), FileAccess::READ);