From 26130f4de8fdc5c9933afb23aaf2604a99773793 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Wed, 19 Nov 2025 17:34:52 +0000 Subject: [PATCH 01/13] Fix bug in reading `FieldPerp` with adios --- src/sys/options/options_adios.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sys/options/options_adios.cxx b/src/sys/options/options_adios.cxx index 4a41a014eb..4342ab5104 100644 --- a/src/sys/options/options_adios.cxx +++ b/src/sys/options/options_adios.cxx @@ -129,7 +129,7 @@ Options readVariable(adios2::Engine& reader, adios2::IO& io, const std::string& return Options(value); } if ((static_cast(dims[0]) == mesh->GlobalNx) - and (static_cast(dims[2]) == mesh->GlobalNz)) { + and (static_cast(dims[1]) == mesh->GlobalNz)) { // Probably a FieldPerp // Read just the local piece of the array From 56287eb9fd4780b5a831d59bd2a7d0542195c738 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Wed, 19 Nov 2025 17:03:15 +0000 Subject: [PATCH 02/13] Add unit tests for `OptionsADIOS2` - Replaces half-implemented integrated tests - Ensure `engine` is always created before use - Ensure `io` is always created before use --- include/bout/adios_object.hxx | 57 +++- src/sys/adios_object.cxx | 14 +- src/sys/options/options_adios.cxx | 86 ++--- src/sys/options/options_adios.hxx | 13 +- tests/integrated/CMakeLists.txt | 1 - .../test-options-adios/CMakeLists.txt | 6 - .../test-options-adios/data/BOUT.inp | 6 - tests/integrated/test-options-adios/runtest | 74 ---- .../test-options-adios/test-options-adios.cxx | 125 ------- tests/unit/CMakeLists.txt | 1 + tests/unit/fake_mesh.hxx | 11 + tests/unit/sys/test_options_adios2.cxx | 322 ++++++++++++++++++ tests/unit/test_tmpfiles.hxx | 2 +- 13 files changed, 431 insertions(+), 287 deletions(-) delete mode 100644 tests/integrated/test-options-adios/CMakeLists.txt delete mode 100644 tests/integrated/test-options-adios/data/BOUT.inp delete mode 100755 tests/integrated/test-options-adios/runtest delete mode 100644 tests/integrated/test-options-adios/test-options-adios.cxx create mode 100644 tests/unit/sys/test_options_adios2.cxx diff --git a/include/bout/adios_object.hxx b/include/bout/adios_object.hxx index 07482a0dcb..cab27d1a0b 100755 --- a/include/bout/adios_object.hxx +++ b/include/bout/adios_object.hxx @@ -16,6 +16,8 @@ #if BOUT_HAS_ADIOS2 +#include "bout/boutexception.hxx" + #include #include #include @@ -36,14 +38,12 @@ IOPtr GetIOPtr(const std::string IOName); class ADIOSStream { public: adios2::IO io; - adios2::Engine engine; adios2::Variable vTime; adios2::Variable vStep; int adiosStep = 0; - bool isInStep = false; // true if BeginStep was called and EndStep was not yet called /** create or return the ADIOSStream based on the target file name */ - static ADIOSStream& ADIOSGetStream(const std::string& fname); + static ADIOSStream& ADIOSGetStream(const std::string& fname, adios2::Mode mode); ~ADIOSStream(); @@ -74,9 +74,58 @@ public: return v; } + auto engine() -> adios2::Engine& { + if (not engine_) { + engine_ = io.Open(fname, file_mode); + if (not engine_) { + throw BoutException("Could not open ADIOS file '{:s}' for writing", fname); + } + } + return engine_; + } + + void beginStep() { + if (not isInStep) { + engine().BeginStep(); + isInStep = true; + adiosStep = static_cast(engine().CurrentStep()); + } + } + + void endStep() { + if (isInStep) { + engine().EndStep(); + isInStep = false; + } + } + + void finish() { + if (engine_) { + engine().EndStep(); + engine().Close(); + } + } + private: - ADIOSStream(const std::string fname) : fname(fname){}; + ADIOSStream(const std::string& fname, adios2::Mode mode) + : fname(fname), file_mode(mode) { + + ADIOSPtr adiosp = GetADIOSPtr(); + std::string ioname = "write_" + fname; + try { + io = adiosp->AtIO(ioname); + } catch (const std::invalid_argument& e) { + io = adiosp->DeclareIO(ioname); + io.SetEngine("BP5"); + } + }; + std::string fname; + adios2::Mode file_mode; + adios2::Engine engine_; + + /// true if BeginStep was called and EndStep was not yet called + bool isInStep = false; }; /** Set user parameters for an IO group */ diff --git a/src/sys/adios_object.cxx b/src/sys/adios_object.cxx index 53d8ccaa84..e2220386ec 100644 --- a/src/sys/adios_object.cxx +++ b/src/sys/adios_object.cxx @@ -5,8 +5,8 @@ #include "bout/adios_object.hxx" #include "bout/boutexception.hxx" -#include -#include +#include + #include namespace bout { @@ -48,19 +48,19 @@ IOPtr GetIOPtr(const std::string IOName) { } ADIOSStream::~ADIOSStream() { - if (engine) { + if (engine_) { if (isInStep) { - engine.EndStep(); + engine_.EndStep(); isInStep = false; } - engine.Close(); + engine_.Close(); } } -ADIOSStream& ADIOSStream::ADIOSGetStream(const std::string& fname) { +ADIOSStream& ADIOSStream::ADIOSGetStream(const std::string& fname, adios2::Mode mode) { auto it = adiosStreams.find(fname); if (it == adiosStreams.end()) { - it = adiosStreams.emplace(fname, ADIOSStream(fname)).first; + it = adiosStreams.emplace(fname, ADIOSStream(fname, mode)).first; } return it->second; } diff --git a/src/sys/options/options_adios.cxx b/src/sys/options/options_adios.cxx index 4342ab5104..e17ef948e2 100644 --- a/src/sys/options/options_adios.cxx +++ b/src/sys/options/options_adios.cxx @@ -4,15 +4,15 @@ #if BOUT_HAS_ADIOS2 #include "options_adios.hxx" -#include "bout/adios_object.hxx" -#include "bout/bout.hxx" +#include "bout/adios_object.hxx" #include "bout/boutexception.hxx" #include "bout/globals.hxx" #include "bout/mesh.hxx" +#include "bout/options.hxx" #include "bout/sys/timer.hxx" -#include "adios2.h" +#include #include #include @@ -34,8 +34,8 @@ OptionsADIOS::OptionsADIOS(Options& options) : OptionsIO(options) { } file_mode = (options["append"].doc("Append to existing file?").withDefault(false)) - ? FileMode::append - : FileMode::replace; + ? adios2::Mode::Append + : adios2::Mode::Write; singleWriteFile = options["singleWriteFile"].withDefault(false); } @@ -305,10 +305,16 @@ Options OptionsADIOS::read([[maybe_unused]] bool lazy) { } void OptionsADIOS::verifyTimesteps() const { - ADIOSStream& stream = ADIOSStream::ADIOSGetStream(filename); - stream.engine.EndStep(); - stream.isInStep = false; - return; + // This doesn't _verify_ the timesteps, but does flush to disk. + // Maybe this is fine, because ADIOS2 doesn't require every variable + // to be in sync? + if (singleWriteFile) { + return; + } + + ADIOSStream& stream = ADIOSStream::ADIOSGetStream(filename, file_mode); + + stream.endStep(); } const std::vector DIMS_NONE; @@ -355,7 +361,7 @@ struct ADIOSPutVarVisitor { template void operator()(const T& value) { adios2::Variable var = stream.GetValueVariable(varname); - stream.engine.Put(var, value); + stream.engine().Put(var, value); } void operator()(const Array& value) { put_helper(value); } @@ -375,7 +381,7 @@ struct ADIOSPutVarVisitor { const auto shape = make_shape(static_cast(BoutComm::size()), value); auto var = stream.GetArrayVariable(varname, shape, DIMS_NONE, BoutComm::rank()); var.SetSelection({make_start(value), make_shape(1, value)}); - stream.engine.Put(var, value.begin()); + stream.engine().Put(var, value.begin()); } }; @@ -386,7 +392,7 @@ void ADIOSPutVarVisitor::operator()(const bool& value) { return; } adios2::Variable var = stream.GetValueVariable(varname); - stream.engine.Put(var, static_cast(value)); + stream.engine().Put(var, static_cast(value)); } template <> @@ -396,7 +402,7 @@ void ADIOSPutVarVisitor::operator()(const int& value) { return; } adios2::Variable var = stream.GetValueVariable(varname); - stream.engine.Put(var, value); + stream.engine().Put(var, value); } template <> @@ -406,7 +412,7 @@ void ADIOSPutVarVisitor::operator()(const BoutReal& value) { return; } adios2::Variable var = stream.GetValueVariable(varname); - stream.engine.Put(var, value); + stream.engine().Put(var, value); } template <> @@ -416,7 +422,7 @@ void ADIOSPutVarVisitor::operator()(const std::string& value) { return; } adios2::Variable var = stream.GetValueVariable(varname); - stream.engine.Put(var, value, adios2::Mode::Sync); + stream.engine().Put(var, value, adios2::Mode::Sync); } template <> @@ -449,7 +455,7 @@ void ADIOSPutVarVisitor::operator()(const Field2D& value) { stream.GetArrayVariable(varname, shape, DIMS_XY, BoutComm::rank()); var.SetSelection({start, count}); var.SetMemorySelection({memStart, memCount}); - stream.engine.Put(var, &value(0, 0)); + stream.engine().Put(var, &value(0, 0)); } template <> @@ -487,7 +493,7 @@ void ADIOSPutVarVisitor::operator()(const Field3D& value) { stream.GetArrayVariable(varname, shape, DIMS_XYZ, BoutComm::rank()); var.SetSelection({start, count}); var.SetMemorySelection({memStart, memCount}); - stream.engine.Put(var, &value(0, 0, 0)); + stream.engine().Put(var, &value(0, 0, 0)); } template <> @@ -520,7 +526,7 @@ void ADIOSPutVarVisitor::operator()(const FieldPerp& value) { stream.GetArrayVariable(varname, shape, DIMS_XZ, BoutComm::rank()); var.SetSelection({start, count}); var.SetMemorySelection({memStart, memCount}); - stream.engine.Put(var, &value(0, 0)); + stream.engine().Put(var, &value(0, 0)); } /// Visit a variant type, and put the data into a NcVar @@ -604,48 +610,18 @@ void OptionsADIOS::write(const Options& options, const std::string& time_dim) { Timer timer("io"); // ADIOSStream is just a BOUT++ object, it does not create anything inside ADIOS - ADIOSStream& stream = ADIOSStream::ADIOSGetStream(filename); - - // Need to have an adios2::IO object first, which can only be created once. - if (!stream.io) { - ADIOSPtr adiosp = GetADIOSPtr(); - std::string ioname = "write_" + filename; - try { - stream.io = adiosp->AtIO(ioname); - } catch (const std::invalid_argument& e) { - stream.io = adiosp->DeclareIO(ioname); - stream.io.SetEngine("BP5"); - } - } - - /* Open file once and keep it open, close in stream desctructor - or close after writing if singleWriteFile == true - */ - if (!stream.engine) { - adios2::Mode amode = - (file_mode == FileMode::append ? adios2::Mode::Append : adios2::Mode::Write); - stream.engine = stream.io.Open(filename, amode); - if (!stream.engine) { - throw BoutException("Could not open ADIOS file '{:s}' for writing", filename); - } - } + ADIOSStream& stream = ADIOSStream::ADIOSGetStream(filename, file_mode); - /* Multiple write() calls allowed in a single adios step to output multiple - Options objects in the same step. verifyTimesteps() will indicate the - completion of the step (and adios will publish the step). - */ - if (!stream.isInStep) { - stream.engine.BeginStep(); - stream.isInStep = true; - stream.adiosStep = stream.engine.CurrentStep(); - } + // Multiple write() calls allowed in a single adios step to output multiple + // Options objects in the same step. verifyTimesteps() will indicate the + // completion of the step (and adios will publish the step). + stream.beginStep(); writeGroup(options, stream, "", time_dim); - /* In singleWriteFile mode, we complete the step and close the file */ + // In singleWriteFile mode, we complete the step and close the file if (singleWriteFile) { - stream.engine.EndStep(); - stream.engine.Close(); + stream.finish(); } } diff --git a/src/sys/options/options_adios.hxx b/src/sys/options/options_adios.hxx index 6e81508ca3..f5b397188d 100644 --- a/src/sys/options/options_adios.hxx +++ b/src/sys/options/options_adios.hxx @@ -5,7 +5,6 @@ #define OPTIONS_ADIOS_H #include "bout/build_defines.hxx" -#include "bout/options.hxx" #include "bout/options_io.hxx" #if !BOUT_HAS_ADIOS2 @@ -17,7 +16,10 @@ bout::RegisterUnavailableOptionsIO #else -#include +#include "bout/options.hxx" + +#include + #include namespace bout { @@ -61,15 +63,10 @@ public: void verifyTimesteps() const override; private: - enum class FileMode { - replace, ///< Overwrite file when writing - append ///< Append to file when writing - }; - /// Name of the file on disk std::string filename; /// How to open the file for writing - FileMode file_mode{FileMode::replace}; + adios2::Mode file_mode{adios2::Mode::Write}; bool singleWriteFile = false; }; diff --git a/tests/integrated/CMakeLists.txt b/tests/integrated/CMakeLists.txt index e11403efb9..3696ef0372 100644 --- a/tests/integrated/CMakeLists.txt +++ b/tests/integrated/CMakeLists.txt @@ -32,7 +32,6 @@ add_subdirectory(test-laplacexz) add_subdirectory(test-multigrid_laplace) add_subdirectory(test-naulin-laplace) add_subdirectory(test-options-netcdf) -add_subdirectory(test-options-adios) add_subdirectory(test-petsc_laplace) add_subdirectory(test-petsc_laplace_MAST-grid) add_subdirectory(test-restart-io) diff --git a/tests/integrated/test-options-adios/CMakeLists.txt b/tests/integrated/test-options-adios/CMakeLists.txt deleted file mode 100644 index cc61fabe57..0000000000 --- a/tests/integrated/test-options-adios/CMakeLists.txt +++ /dev/null @@ -1,6 +0,0 @@ -bout_add_integrated_test(test-options-adios - SOURCES test-options-adios.cxx - USE_RUNTEST - USE_DATA_BOUT_INP - REQUIRES BOUT_HAS_ADIOS2 - ) diff --git a/tests/integrated/test-options-adios/data/BOUT.inp b/tests/integrated/test-options-adios/data/BOUT.inp deleted file mode 100644 index fa0f6d3681..0000000000 --- a/tests/integrated/test-options-adios/data/BOUT.inp +++ /dev/null @@ -1,6 +0,0 @@ - - -[mesh] -nx = 5 -ny = 2 -nz = 2 diff --git a/tests/integrated/test-options-adios/runtest b/tests/integrated/test-options-adios/runtest deleted file mode 100755 index 03a83fc0ba..0000000000 --- a/tests/integrated/test-options-adios/runtest +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env python3 - -# Note: This test requires NCDF4, whereas on Travis NCDF is used -# requires: netcdf -# requires: adios -# requires: not legacy_netcdf - -from boututils.datafile import DataFile -from boututils.run_wrapper import build_and_log, shell, launch -from boutdata.data import BoutOptionsFile - -import math -import numpy as np - -build_and_log("options-netcdf test") -shell("rm -f test-out.ini") -shell("rm -f test-out.nc") - -# Create a NetCDF input file -with DataFile("test.nc", create=True, format="NETCDF4") as f: - f.write("int", 42) - f.write("real", 3.1415) - f.write("string", "hello") - -# run BOUT++ -launch("./test-options-adios", nproc=1, mthread=1) - -# Check the output INI file -result = BoutOptionsFile("test-out.ini") - -print(result) - -assert result["int"] == 42 -assert math.isclose(result["real"], 3.1415) -assert result["string"] == "hello" - -print("Checking saved ADIOS2 test-out file -- Not implemented") - -# Check the output NetCDF file -# with DataFile("test-out.nc") as f: -# assert f["int"] == 42 -# assert math.isclose(f["real"], 3.1415) -# assert result["string"] == "hello" - -print("Checking saved settings.ini") - -# Check the settings.ini file, coming from BOUT.inp -# which is converted to NetCDF, read in, then written again -settings = BoutOptionsFile("settings.ini") - -assert settings["mesh"]["nx"] == 5 -assert settings["mesh"]["ny"] == 2 - -print("Checking saved fields.bp -- Not implemented") - -# with DataFile("fields.nc") as f: -# assert f["f2d"].shape == (5, 6) # Field2D -# assert f["f3d"].shape == (5, 6, 2) # Field3D -# assert f["fperp"].shape == (5, 2) # FieldPerp -# assert np.allclose(f["f2d"], 1.0) -# assert np.allclose(f["f3d"], 2.0) -# assert np.allclose(f["fperp"], 3.0) - -print("Checking saved fields2.bp -- Not implemented") - -# with DataFile("fields2.nc") as f: -# assert f["f2d"].shape == (5, 6) # Field2D -# assert f["f3d"].shape == (5, 6, 2) # Field3D -# assert f["fperp"].shape == (5, 2) # FieldPerp -# assert np.allclose(f["f2d"], 1.0) -# assert np.allclose(f["f3d"], 2.0) -# assert np.allclose(f["fperp"], 3.0) - -print(" => Passed") diff --git a/tests/integrated/test-options-adios/test-options-adios.cxx b/tests/integrated/test-options-adios/test-options-adios.cxx deleted file mode 100644 index ba0bbe3898..0000000000 --- a/tests/integrated/test-options-adios/test-options-adios.cxx +++ /dev/null @@ -1,125 +0,0 @@ - -#include "bout/bout.hxx" - -#include "bout/options_io.hxx" -#include "bout/optionsreader.hxx" -#include "bout/utils.hxx" - -using bout::OptionsIO; - -int main(int argc, char** argv) { - BoutInitialise(argc, argv); - - // Read values from a NetCDF file - auto file = bout::OptionsIO::create("test.nc"); - - auto values = file->read(); - - values.printUnused(); - - // Write to an INI text file - OptionsReader* reader = OptionsReader::getInstance(); - reader->write(&values, "test-out.ini"); - - // Write to ADIOS file, by setting file type "adios" - OptionsIO::create({{"file", "test-out.bp"}, - {"type", "adios"}, - {"append", false}, - {"singleWriteFile", true}}) - ->write(values); - - /////////////////////////// - - // Write the BOUT.inp settings to ADIOS file - OptionsIO::create({{"file", "settings.bp"}, - {"type", "adios"}, - {"append", false}, - {"singleWriteFile", true}}) - ->write(Options::root()); - - // Read back in - auto settings = OptionsIO::create({{"file", "settings.bp"}, {"type", "adios"}})->read(); - - // Write to INI file - reader->write(&settings, "settings.ini"); - - /////////////////////////// - // Write fields - - { - constexpr int nx = 2; - constexpr int ny = 3; - Matrix matrix_in(nx, ny); - int count = 0; - - for (int i = 0; i < nx; ++i) { - for (int j = 0; j < ny; ++j) { - matrix_in(i, j) = ++count; - } - } - - Options fields{{"f2d", Field2D(1.0)}, - {"f3d", Field3D(2.0)}, - {"fperp", FieldPerp(3.0)}, - {"matrix", matrix_in}}; - auto f = OptionsIO::create({{"file", "fields.bp"}, {"type", "adios"}}); - /* - write() for adios only buffers data but does not guarantee writing to disk - unless singleWriteFile is set to true - */ - f->write(fields); - // indicate completion of step, required to get data on disk - f->verifyTimesteps(); - } - - /////////////////////////// - // Read fields - - Options fields_in = - OptionsIO::create({{"file", "fields.bp"}, {"type", "adios"}})->read(); - - auto f2d = fields_in["f2d"].as(bout::globals::mesh); - auto f3d = fields_in["f3d"].as(bout::globals::mesh); - auto fperp = fields_in["fperp"].as(bout::globals::mesh); - - Options fields2; - fields2["f2d"] = f2d; - fields2["f3d"] = f3d; - fields2["fperp"] = fperp; - - // Write out again - auto f2 = OptionsIO::create({{"file", "fields2.bp"}, - {"type", "adios"}, - {"append", false}, - {"singleWriteFile", true}}); - f2->write(fields2); - - /////////////////////////// - // Time dependent values - - Options data; - data["scalar"] = 1.0; - data["scalar"].attributes["time_dimension"] = "t"; - - data["field"] = Field3D(2.0); - data["field"].attributes["time_dimension"] = "t"; - - OptionsIO::create({{"file", "time.bp"}, - {"type", "adios"}, - {"append", false}, - {"singleWriteFile", true}}) - ->write(data); - - // Update time-dependent values - data["scalar"] = 2.0; - data["field"] = Field3D(3.0); - - // Append data to file - OptionsIO::create({{"file", "time.bp"}, - {"type", "adios"}, - {"append", true}, - {"singleWriteFile", true}}) - ->write(data); - - BoutFinalise(); -}; diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 47253c508f..40aa207dea 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -80,6 +80,7 @@ set(serial_tests_source ./sys/test_expressionparser.cxx ./sys/test_msg_stack.cxx ./sys/test_options.cxx + ./sys/test_options_adios2.cxx ./sys/test_options_fields.cxx ./sys/test_options_netcdf.cxx ./sys/test_optionsreader.cxx diff --git a/tests/unit/fake_mesh.hxx b/tests/unit/fake_mesh.hxx index e6f78f8767..b945b46524 100644 --- a/tests/unit/fake_mesh.hxx +++ b/tests/unit/fake_mesh.hxx @@ -50,6 +50,17 @@ public: OffsetY = 0; OffsetZ = 0; + // These bits only for ADIOS2, also boring due to single process + MapCountX = nx - 2; + MapCountY = ny - 2; + MapCountZ = nz; + MapGlobalX = nx; + MapGlobalY = ny; + MapGlobalZ = nz; + MapLocalX = nx - 2; + MapLocalY = ny - 2; + MapLocalZ = nz; + // Small "inner" region xstart = 1; xend = nx - 2; diff --git a/tests/unit/sys/test_options_adios2.cxx b/tests/unit/sys/test_options_adios2.cxx new file mode 100644 index 0000000000..d5bb4653a6 --- /dev/null +++ b/tests/unit/sys/test_options_adios2.cxx @@ -0,0 +1,322 @@ +// Test reading and writing to ADIOS2 + +#include "bout/build_defines.hxx" + +#if BOUT_HAS_ADIOS2 + +#include "gtest/gtest.h" + +#include "test_extras.hxx" +#include "test_tmpfiles.hxx" +#include "bout/adios_object.hxx" +#include "bout/field3d.hxx" +#include "bout/mesh.hxx" +#include "bout/options_io.hxx" +#include "bout/utils.hxx" + +using bout::OptionsIO; + +#include + +#include "fake_mesh_fixture.hxx" + +// Reuse the "standard" fixture for FakeMesh +class OptionsAdios2Test : public FakeMeshFixture { +public: + static void SetUpTestSuite() { bout::ADIOSInit(BoutComm::get()); } + static void TearDownTestSuite() { bout::ADIOSFinalize(); } + + ~OptionsAdios2Test() override { + bout::ADIOSStream::ADIOSGetStream(filename, adios2::Mode::Deferred).engine().Close(); + } + + // A temporary filename + bout::testing::TempFile filename; + WithQuietOutput quiet{output_info}; + Options file_options{ + {"file", filename.string()}, + {"type", "adios"}, + {"singleWriteFile", true}, + }; + Options no_write_options{ + {"file", filename.string()}, + {"type", "adios"}, + {"singleWriteFile", false}, + }; +}; + +TEST_F(OptionsAdios2Test, ReadWriteInt) { + { + Options options; + options["test"] = 42; + + // Write the file + OptionsIO::create(file_options)->write(options); + } + + // Read again + Options data = OptionsIO::create(file_options)->read(); + + EXPECT_EQ(data["test"], 42); +} + +TEST_F(OptionsAdios2Test, ReadWriteString) { + { + Options options; + options["test"] = std::string{"hello"}; + + // Write file + OptionsIO::create(file_options)->write(options); + } + + // Read file + Options data = OptionsIO::create(file_options)->read(); + + EXPECT_EQ(data["test"], std::string("hello")); +} + +TEST_F(OptionsAdios2Test, ReadWriteField2D) { + { + Options options; + options["test"] = Field2D(1.0); + + // Write file + OptionsIO::create(file_options)->write(options); + } + + // Read file + Options data = OptionsIO::create(file_options)->read(); + + Field2D value = data["test"].as(bout::globals::mesh); + + EXPECT_DOUBLE_EQ(value(0, 1), 1.0); + EXPECT_DOUBLE_EQ(value(1, 0), 1.0); +} + +TEST_F(OptionsAdios2Test, ReadWriteField3D) { + { + Options options; + options["test"] = Field3D(2.4); + + // Write file + OptionsIO::create(file_options)->write(options); + } + + // Read file + Options data = OptionsIO::create(file_options)->read(); + + Field3D value = data["test"].as(bout::globals::mesh); + + EXPECT_DOUBLE_EQ(value(0, 1, 0), 2.4); + EXPECT_DOUBLE_EQ(value(1, 0, 1), 2.4); + EXPECT_DOUBLE_EQ(value(1, 1, 1), 2.4); +} + +TEST_F(OptionsAdios2Test, Groups) { + { + Options options; + options["test"]["key"] = 42; + + // Write file + OptionsIO::create(file_options)->write(options); + } + + // Read file + Options data = OptionsIO::create(file_options)->read(); + EXPECT_EQ(data["test"]["key"], 42); +} + +TEST_F(OptionsAdios2Test, AttributeInt) { + { + Options options; + options["test"] = 3; + options["test"].attributes["thing"] = 4; + + // Write file + OptionsIO::create(file_options)->write(options); + } + + // Read file + Options data = OptionsIO::create(file_options)->read(); + EXPECT_EQ(data["test"].attributes["thing"].as(), 4); +} + +TEST_F(OptionsAdios2Test, AttributeBoutReal) { + { + Options options; + options["test"] = 3; + options["test"].attributes["thing"] = 3.14; + + // Write file + OptionsIO::create(file_options)->write(options); + } + + // Read file + Options data = OptionsIO::create(file_options)->read(); + EXPECT_DOUBLE_EQ(data["test"].attributes["thing"].as(), 3.14); +} + +TEST_F(OptionsAdios2Test, AttributeString) { + { + Options options; + options["test"] = 3; + options["test"].attributes["thing"] = "hello"; + + // Write file + OptionsIO::create(file_options)->write(options); + } + + // Read file + Options data = OptionsIO::create(file_options)->read(); + EXPECT_EQ(data["test"].attributes["thing"].as(), "hello"); +} + +TEST_F(OptionsAdios2Test, Field2DWriteCellCentre) { + { + Options options; + options["f2d"] = Field2D(2.0); + + // Write file + OptionsIO::create(file_options)->write(options); + } + + // Read file + Options data = OptionsIO::create(file_options)->read(); + + EXPECT_EQ(data["f2d"].attributes["cell_location"].as(), + toString(CELL_CENTRE)); +} + +TEST_F(OptionsAdios2Test, Field2DWriteCellYLow) { + { + Options options; + options["f2d"] = Field2D(2.0, mesh_staggered).setLocation(CELL_YLOW); + + // Write file + OptionsIO::create(file_options)->write(options); + } + + // Read file + Options data = OptionsIO::create(file_options)->read(); + + EXPECT_EQ(data["f2d"].attributes["cell_location"].as(), + toString(CELL_YLOW)); +} + +TEST_F(OptionsAdios2Test, Field3DWriteCellCentre) { + { + Options options; + options["f3d"] = Field3D(2.0); + + // Write file + OptionsIO::create(file_options)->write(options); + } + + // Read file + Options data = OptionsIO::create(file_options)->read(); + + EXPECT_EQ(data["f3d"].attributes["cell_location"].as(), + toString(CELL_CENTRE)); +} + +TEST_F(OptionsAdios2Test, Field3DWriteCellYLow) { + { + Options options; + options["f3d"] = Field3D(2.0, mesh_staggered).setLocation(CELL_YLOW); + + // Write file + OptionsIO::create(file_options)->write(options); + } + + // Read file + Options data = OptionsIO::create(file_options)->read(); + + EXPECT_EQ(data["f3d"].attributes["cell_location"].as(), + toString(CELL_YLOW)); +} + +TEST_F(OptionsAdios2Test, FieldPerpWriteCellCentre) { + { + Options options; + FieldPerp fperp(3.0); + fperp.setIndex(2); + options["fperp"] = fperp; + + // Ensure MPI is initialised, otherwise we end up creating threads while + // the file is open, and the lock is not removed on closing. + fperp.getMesh()->getXcomm(); + + // Write file + OptionsIO::create(file_options)->write(options); + } + + // Read file + Options data = OptionsIO::create(file_options)->read(); + + EXPECT_EQ(data["fperp"].attributes["cell_location"].as(), + toString(CELL_CENTRE)); + EXPECT_EQ(data["fperp"].attributes["yindex_global"].as(), 2); +} + +TEST_F(OptionsAdios2Test, VerifyTimesteps) { + { + Options options; + options["thing1"] = 1.0; + options["thing1"].attributes["time_dimension"] = "t"; + + auto f = OptionsIO::create(no_write_options); + f->write(options); + EXPECT_NO_THROW(f->verifyTimesteps()); + } + + { + Options options; + options["thing1"] = 2.0; + options["thing1"].attributes["time_dimension"] = "t"; + + options["thing2"] = 3.0; + options["thing2"].attributes["time_dimension"] = "t"; + + OptionsIO::create({{"type", "adios"}, {"file", filename.string()}, {"append", true}}) + ->write(options); + } + + // Doesn't throw, but it's less of a problem for ADIOS2 than netCDF? + // EXPECT_THROW(OptionsIO::create(no_write_options)->verifyTimesteps(), BoutException); +} + +TEST_F(OptionsAdios2Test, WriteTimeDimension) { + { + Options options; + options["thing1"].assignRepeat(1.0); // default time dim + options["thing2"].assignRepeat(2.0, "t2"); // non-default + + // Only write non-default time dim + OptionsIO::create(file_options)->write(options, "t2"); + } + + Options data = OptionsIO::create(file_options)->read(); + + EXPECT_FALSE(data.isSet("thing1")); + EXPECT_TRUE(data.isSet("thing2")); +} + +TEST_F(OptionsAdios2Test, WriteMultipleTimeDimensions) { + { + Options options; + options["thing1_t1"].assignRepeat(1.0); // default time dim + options["thing2_t1"].assignRepeat(1.0); // default time dim + + options["thing3_t2"].assignRepeat(2.0, "t2"); // non-default + options["thing4_t2"].assignRepeat(2.0, "t2"); // non-default + + // Write the non-default time dim twice + OptionsIO::create(no_write_options)->write(options, "t2"); + OptionsIO::create(no_write_options)->write(options, "t2"); + OptionsIO::create(no_write_options)->write(options, "t"); + } + + EXPECT_NO_THROW(OptionsIO::create(no_write_options)->verifyTimesteps()); +} + +#endif // Bout_Has_ADIOS2 diff --git a/tests/unit/test_tmpfiles.hxx b/tests/unit/test_tmpfiles.hxx index 35d7579df2..6c37e7c792 100644 --- a/tests/unit/test_tmpfiles.hxx +++ b/tests/unit/test_tmpfiles.hxx @@ -30,7 +30,7 @@ public: TempFile& operator=(const TempFile&) = delete; TempFile& operator=(TempFile&&) = delete; - ~TempFile() { std::filesystem::remove(filename); } + ~TempFile() { std::filesystem::remove_all(filename); } // Enable conversions to std::string / const char* operator std::string() const { return filename.string(); } From 8f2c0d1185c380d8d02445a208a9fcd36aa2a5d1 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Wed, 19 Nov 2025 17:35:27 +0000 Subject: [PATCH 03/13] Fix some clang-tidy warnings --- include/bout/adios_object.hxx | 4 ++-- src/sys/adios_object.cxx | 6 +++--- src/sys/options/options_adios.hxx | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/include/bout/adios_object.hxx b/include/bout/adios_object.hxx index cab27d1a0b..fcade4f55c 100755 --- a/include/bout/adios_object.hxx +++ b/include/bout/adios_object.hxx @@ -129,8 +129,8 @@ private: }; /** Set user parameters for an IO group */ -void ADIOSSetParameters(const std::string& input, const char delimKeyValue, - const char delimItem, adios2::IO& io); +void ADIOSSetParameters(const std::string& input, char delimKeyValue, char delimItem, + adios2::IO& io); } // namespace bout diff --git a/src/sys/adios_object.cxx b/src/sys/adios_object.cxx index e2220386ec..83bdf45f27 100644 --- a/src/sys/adios_object.cxx +++ b/src/sys/adios_object.cxx @@ -65,8 +65,8 @@ ADIOSStream& ADIOSStream::ADIOSGetStream(const std::string& fname, adios2::Mode return it->second; } -void ADIOSSetParameters(const std::string& input, const char delimKeyValue, - const char delimItem, adios2::IO& io) { +void ADIOSSetParameters(const std::string& input, char delimKeyValue, + char delimItem, adios2::IO& io) { auto lf_Trim = [](std::string& input) { input.erase(0, input.find_first_not_of(" \n\r\t")); // prefixing spaces input.erase(input.find_last_not_of(" \n\r\t") + 1); // suffixing spaces @@ -76,7 +76,7 @@ void ADIOSSetParameters(const std::string& input, const char delimKeyValue, std::string parameter; while (std::getline(inputSS, parameter, delimItem)) { const size_t position = parameter.find(delimKeyValue); - if (position == parameter.npos) { + if (position == std::string::npos) { throw BoutException("ADIOSSetParameters(): wrong format for IO parameter " + parameter + ", format must be key" + delimKeyValue + "value for each entry"); diff --git a/src/sys/options/options_adios.hxx b/src/sys/options/options_adios.hxx index f5b397188d..534c3e1707 100644 --- a/src/sys/options/options_adios.hxx +++ b/src/sys/options/options_adios.hxx @@ -46,7 +46,7 @@ public: OptionsADIOS(const OptionsADIOS&) = delete; OptionsADIOS(OptionsADIOS&&) noexcept = default; - ~OptionsADIOS() = default; + ~OptionsADIOS() override = default; OptionsADIOS& operator=(const OptionsADIOS&) = delete; OptionsADIOS& operator=(OptionsADIOS&&) noexcept = default; From 602de3d622deb12b3aa7dbca3e7eca883b1904f3 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Thu, 20 Nov 2025 09:43:46 +0000 Subject: [PATCH 04/13] Remove unused constant --- src/sys/options/options_adios.cxx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/sys/options/options_adios.cxx b/src/sys/options/options_adios.cxx index e17ef948e2..24e92544c4 100644 --- a/src/sys/options/options_adios.cxx +++ b/src/sys/options/options_adios.cxx @@ -21,9 +21,6 @@ #include namespace bout { -/// Name of the attribute used to track individual variable's time indices -constexpr auto current_time_index_name = "current_time_index"; - OptionsADIOS::OptionsADIOS(Options& options) : OptionsIO(options) { if (options["file"].doc("File name. Defaults to /.pb").isSet()) { filename = options["file"].as(); From 80cf88d16a19ef9441a60c7d5f02d99061db71ba Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Thu, 20 Nov 2025 09:47:41 +0000 Subject: [PATCH 05/13] Switch to `BoutException` --- src/sys/options/options_adios.cxx | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/sys/options/options_adios.cxx b/src/sys/options/options_adios.cxx index 24e92544c4..28eba431d3 100644 --- a/src/sys/options/options_adios.cxx +++ b/src/sys/options/options_adios.cxx @@ -52,30 +52,30 @@ Options readVariable(adios2::Engine& reader, adios2::IO& io, const std::string& } if (variable.ShapeID() == adios2::ShapeID::LocalArray) { - throw std::invalid_argument( - "ADIOS reader did not implement reading local arrays like " + type + " " + name - + " in file " + reader.Name()); + throw BoutException( + "ADIOS reader did not implement reading local arrays like `{}` '{}' in file '{}'", + type, name, reader.Name()); } if (type != "double" && type != "float") { - throw std::invalid_argument( - "ADIOS reader did not implement reading arrays that are not double/float type. " - "Found " - + type + " " + name + " in file " + reader.Name()); + throw BoutException("ADIOS reader did not implement reading arrays that are not " + "`double`/`float` type. " + "Found `{}` '{}' in file '{}'", + type, name, reader.Name()); } if (type == "double" && sizeof(BoutReal) != sizeof(double)) { - throw std::invalid_argument( + throw BoutException( "ADIOS does not allow for implicit type conversions. BoutReal type is " - "float but found " - + type + " " + name + " in file " + reader.Name()); + "float but found `{}` '{}' in file '{}'", + type, name, reader.Name()); } if (type == "float" && sizeof(BoutReal) != sizeof(float)) { - throw std::invalid_argument( + throw BoutException( "ADIOS reader does not allow for implicit type conversions. BoutReal type is " - "double but found " - + type + " " + name + " in file " + reader.Name()); + "double but found `{}` '{}' in file '{}'", + type, name, reader.Name()); } auto dims = variable.Shape(); From 4dba6a97f8a468350d80ef50217fa8e720c854e6 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Thu, 20 Nov 2025 13:45:44 +0000 Subject: [PATCH 06/13] Add some helper variables for options_adios --- src/sys/options/options_adios.cxx | 41 ++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/src/sys/options/options_adios.cxx b/src/sys/options/options_adios.cxx index 28eba431d3..a1ddc71326 100644 --- a/src/sys/options/options_adios.cxx +++ b/src/sys/options/options_adios.cxx @@ -1,5 +1,4 @@ #include "bout/build_defines.hxx" -#include "bout/traits.hxx" #if BOUT_HAS_ADIOS2 @@ -11,15 +10,27 @@ #include "bout/mesh.hxx" #include "bout/options.hxx" #include "bout/sys/timer.hxx" +#include "bout/traits.hxx" #include +#include #include #include -#include +#include #include #include +namespace { +auto to_int_dims(const adios2::Dims& dims) { + std::vector int_dims; + int_dims.reserve(dims.size()); + std::transform(dims.begin(), dims.end(), std::back_inserter(int_dims), + [](auto dim) { return static_cast(dim); }); + return int_dims; +} +} // namespace + namespace bout { OptionsADIOS::OptionsADIOS(Options& options) : OptionsIO(options) { if (options["file"].doc("File name. Defaults to /.pb").isSet()) { @@ -78,13 +89,18 @@ Options readVariable(adios2::Engine& reader, adios2::IO& io, const std::string& type, name, reader.Name()); } - auto dims = variable.Shape(); - auto ndims = dims.size(); + const auto dims = to_int_dims(variable.Shape()); + const auto ndims = dims.size(); adios2::Variable variableD = io.InquireVariable(name); + const bool dim0_is_x = ndims > 0 ? (dims[0] == mesh->GlobalNx) : false; + const bool dim1_is_y = ndims > 1 ? (dims[1] == mesh->GlobalNy) : false; + const bool dim1_is_z = ndims > 1 ? (dims[1] == mesh->GlobalNz) : false; + const bool dim2_is_z = ndims > 2 ? (dims[2] == mesh->GlobalNz) : false; + switch (ndims) { case 1: { - Array value(static_cast(dims[0])); + Array value(dims[0]); BoutReal* data = value.begin(); reader.Get(variableD, data, adios2::Mode::Sync); return Options(value); @@ -97,8 +113,7 @@ Options readVariable(adios2::Engine& reader, adios2::IO& io, const std::string& // - Add an attribute to specify field type or dimension labels // - Load all the data, and select a region when converting to a Field in Options // - Add a lazy loading type to Options, and load data when needed - if ((static_cast(dims[0]) == mesh->GlobalNx) - and (static_cast(dims[1]) == mesh->GlobalNy)) { + if (dim0_is_x and dim1_is_y) { // Probably a Field2D // Read just the local piece of the array @@ -125,8 +140,7 @@ Options readVariable(adios2::Engine& reader, adios2::IO& io, const std::string& reader.Get(variableD, data, adios2::Mode::Sync); return Options(value); } - if ((static_cast(dims[0]) == mesh->GlobalNx) - and (static_cast(dims[1]) == mesh->GlobalNz)) { + if (dim0_is_x and dim1_is_z) { // Probably a FieldPerp // Read just the local piece of the array @@ -153,15 +167,13 @@ Options readVariable(adios2::Engine& reader, adios2::IO& io, const std::string& reader.Get(variableD, data, adios2::Mode::Sync); return Options(value); } - Matrix value(static_cast(dims[0]), static_cast(dims[1])); + Matrix value(dims[0], dims[1]); BoutReal* data = value.begin(); reader.Get(variableD, data, adios2::Mode::Sync); return Options(value); } case 3: { - if ((static_cast(dims[0]) == mesh->GlobalNx) - and (static_cast(dims[1]) == mesh->GlobalNy) - and (static_cast(dims[2]) == mesh->GlobalNz)) { + if (dim0_is_x and dim1_is_y and dim2_is_z) { // Global array. Read just this processor's part of it Tensor value(mesh->LocalNx, mesh->LocalNy, mesh->LocalNz); @@ -194,8 +206,7 @@ Options readVariable(adios2::Engine& reader, adios2::IO& io, const std::string& } // Doesn't match global array size. // Read the entire array, in case it can be handled later - Tensor value(static_cast(dims[0]), static_cast(dims[1]), - static_cast(dims[2])); + Tensor value(dims[0], dims[1], dims[2]); BoutReal* data = value.begin(); reader.Get(variableD, data, adios2::Mode::Sync); return Options(value); From 5645907a04fe194363375ddb582475f773a2038d Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Thu, 20 Nov 2025 17:44:53 +0000 Subject: [PATCH 07/13] Support reading `Array/Matrix/Tensor` in `OptionsAdios2` --- src/sys/options/options_adios.cxx | 290 +++++++++++++++---------- tests/unit/sys/test_options_adios2.cxx | 24 ++ 2 files changed, 195 insertions(+), 119 deletions(-) diff --git a/src/sys/options/options_adios.cxx b/src/sys/options/options_adios.cxx index a1ddc71326..10cc4ddf6a 100644 --- a/src/sys/options/options_adios.cxx +++ b/src/sys/options/options_adios.cxx @@ -5,6 +5,7 @@ #include "options_adios.hxx" #include "bout/adios_object.hxx" +#include "bout/bout_types.hxx" #include "bout/boutexception.hxx" #include "bout/globals.hxx" #include "bout/mesh.hxx" @@ -13,11 +14,14 @@ #include "bout/traits.hxx" #include +#include +#include #include #include #include #include +#include #include #include @@ -29,6 +33,119 @@ auto to_int_dims(const adios2::Dims& dims) { [](auto dim) { return static_cast(dim); }); return int_dims; } + +struct Selection { + // Offset of this processor's data into the global array + adios2::Dims start; + // The size of the mapped region + adios2::Dims count; + // Where the actual data starts in data pointer (to exclude ghost cells) + adios2::Dims mem_start; + // The actual size of data pointer in memory (including ghost cells) + adios2::Dims mem_count; + // Shape of the local variable to read into + std::vector dims; + + // Distributed Field/Array/Matrix/Tensor + bool should_set_selection{false}; + + Selection(const std::vector& dim_names, const std::vector& dim_sizes, + const Mesh& mesh) { + const auto ndims = dim_names.size(); + const bool dim0_is_rank = ndims > 0 ? (dim_names[0] == "rank") : false; + const bool dim0_is_x = ndims > 0 ? (dim_names[0] == "x") : false; + const bool dim1_is_y = ndims > 1 ? (dim_names[1] == "y") : false; + const bool dim1_is_z = ndims > 1 ? (dim_names[1] == "z") : false; + const bool dim2_is_z = ndims > 2 ? (dim_names[2] == "z") : false; + + should_set_selection = dim0_is_rank or (ndims == 2 and dim0_is_x and dim1_is_y) + or (ndims == 2 and dim0_is_x and dim1_is_z) + or (ndims == 3 and dim0_is_x and dim1_is_y and dim2_is_z); + + if (dim0_is_rank) { + const auto ndim_sizes = dim_sizes.size(); + ASSERT3(ndim_sizes > 1); + + // This is a distributed array, so the local variable is going + // to be shape (dim_sizes[1]...) (that is, drop the rank) + dims.push_back(dim_sizes[1]); + // but we tell adios to read our rank's bit with the full ndims + start = {static_cast(BoutComm::rank()), 0}; + count = {std::size_t{1}, static_cast(dim_sizes[0])}; + + if (ndim_sizes > 2) { + dims.push_back(dim_sizes[2]); + start.push_back(0); + count.push_back(dim_sizes[1]); + } + if (ndim_sizes > 3) { + dims.push_back(dim_sizes[3]); + start.push_back(0); + count.push_back(dim_sizes[2]); + } + mem_count = count; + mem_start = start; + return; + } + + if (ndims > 0) { + dims.push_back(dim0_is_x ? mesh.LocalNx : dim_sizes[0]); + } + if (ndims > 1) { + if (dim1_is_y) { + dims.push_back(mesh.LocalNy); + } else if (dim1_is_z) { + dims.push_back(mesh.LocalNz); + } else { + dims.push_back(dim_sizes[1]); + } + } + if (ndims > 2) { + dims.push_back(dim2_is_z ? mesh.LocalNz : dim_sizes[2]); + } + + start.push_back(static_cast(mesh.MapGlobalX)); + count.push_back(static_cast(mesh.MapCountX)); + mem_start.push_back(static_cast(mesh.MapLocalX)); + mem_count.push_back(static_cast(mesh.LocalNx)); + + if (dim1_is_y) { + start.push_back(static_cast(mesh.MapGlobalY)); + count.push_back(static_cast(mesh.MapCountY)); + mem_start.push_back(static_cast(mesh.MapLocalY)); + mem_count.push_back(static_cast(mesh.LocalNy)); + } else if (dim1_is_z) { + start.push_back(static_cast(mesh.MapGlobalZ)); + count.push_back(static_cast(mesh.MapCountZ)); + mem_start.push_back(static_cast(mesh.MapLocalZ)); + mem_count.push_back(static_cast(mesh.LocalNz)); + } + + if (dim2_is_z) { + start.push_back(static_cast(mesh.MapGlobalZ)); + count.push_back(static_cast(mesh.MapCountZ)); + mem_start.push_back(static_cast(mesh.MapLocalZ)); + mem_count.push_back(static_cast(mesh.LocalNz)); + } + } + + auto selection() const { return adios2::Box{start, count}; } + auto memorySelection() const { return adios2::Box{mem_start, mem_count}; } +}; + +template