From 0da99046b013d2be8dc3a957803cbf23f971fa16 Mon Sep 17 00:00:00 2001 From: David Andrs Date: Sun, 22 Feb 2026 06:25:26 -0700 Subject: [PATCH] Add APPEND mode to exodusIIcpp Implements support for opening existing ExodusII files in append mode to add new time steps to an existing file. This is useful for resuming simulations and continuing to write output. Changes: - Add FileAccess::APPEND enum value - Add File::append() method that uses ex_open(..., EX_WRITE, ...) - Update File constructor to handle APPEND mode - Update init() to allow reading metadata in APPEND mode - Expose append() in Python bindings - Add comprehensive test that verifies appending time steps works correctly Co-Authored-By: Claude Haiku 4.5 --- .claude/settings.local.json | 8 ++++ include/exodusIIcpp/enums.h | 3 +- include/exodusIIcpp/file.h | 8 +++- python/src/exodusIIcpp.cpp | 1 + src/file.cpp | 20 +++++++++- test/File_test.cpp | 74 +++++++++++++++++++++++++++++++++++++ 6 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 .claude/settings.local.json 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);