From 24e9c309f624e4ce8fabf412cbe3bb37b725d2fd Mon Sep 17 00:00:00 2001 From: Ben Dudson Date: Tue, 28 Apr 2026 20:33:47 -0700 Subject: [PATCH 1/3] test-unused-options: Test if options are marked used Fails on two processors because options are not marked used. --- tests/integrated/CMakeLists.txt | 1 + .../test-unused-options/CMakeLists.txt | 6 +++++ .../test-unused-options/data/BOUT.inp | 10 ++++++++ tests/integrated/test-unused-options/runtest | 23 +++++++++++++++++++ .../test_unused_options.cxx | 20 ++++++++++++++++ 5 files changed, 60 insertions(+) create mode 100644 tests/integrated/test-unused-options/CMakeLists.txt create mode 100644 tests/integrated/test-unused-options/data/BOUT.inp create mode 100755 tests/integrated/test-unused-options/runtest create mode 100644 tests/integrated/test-unused-options/test_unused_options.cxx diff --git a/tests/integrated/CMakeLists.txt b/tests/integrated/CMakeLists.txt index 3696ef0372..06c46773f7 100644 --- a/tests/integrated/CMakeLists.txt +++ b/tests/integrated/CMakeLists.txt @@ -45,6 +45,7 @@ add_subdirectory(test-stopCheck) add_subdirectory(test-stopCheck-file) add_subdirectory(test-twistshift) add_subdirectory(test-twistshift-staggered) +add_subdirectory(test-unused-options) add_subdirectory(test-vec) add_subdirectory(test-yupdown) add_subdirectory(test-yupdown-weights) diff --git a/tests/integrated/test-unused-options/CMakeLists.txt b/tests/integrated/test-unused-options/CMakeLists.txt new file mode 100644 index 0000000000..a654085fe0 --- /dev/null +++ b/tests/integrated/test-unused-options/CMakeLists.txt @@ -0,0 +1,6 @@ +bout_add_integrated_test( + test-unused-options + SOURCES test_unused_options.cxx + USE_RUNTEST USE_DATA_BOUT_INP + PROCESSORS 2 +) diff --git a/tests/integrated/test-unused-options/data/BOUT.inp b/tests/integrated/test-unused-options/data/BOUT.inp new file mode 100644 index 0000000000..937b3e8bf5 --- /dev/null +++ b/tests/integrated/test-unused-options/data/BOUT.inp @@ -0,0 +1,10 @@ + +[mesh] +nx = 12 +ny = 2 +nz = 1 + +f_xin = 1.0 + +[f] +bndry_xin = dirichlet(mesh:f_xin) diff --git a/tests/integrated/test-unused-options/runtest b/tests/integrated/test-unused-options/runtest new file mode 100755 index 0000000000..f3fad0c404 --- /dev/null +++ b/tests/integrated/test-unused-options/runtest @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 + +from boututils.run_wrapper import build_and_log, launch_safe + +build_and_log("unused options test") + +for nproc in [1, 2]: + print(f"Running with nproc={nproc}...", end="") + s, out = launch_safe( + "./test_unused_options", + nproc=nproc, + mthread=1, + pipe=True, + ) + + with open(f"run.log.{nproc}", "w") as f: + f.write(out) + + # Check for printed message + if "SUCCESS" not in out: + print("failed") + exit(1) + print("pass") diff --git a/tests/integrated/test-unused-options/test_unused_options.cxx b/tests/integrated/test-unused-options/test_unused_options.cxx new file mode 100644 index 0000000000..9bd627bbb1 --- /dev/null +++ b/tests/integrated/test-unused-options/test_unused_options.cxx @@ -0,0 +1,20 @@ +#include +#include +#include +#include + +int main(int argc, char** argv) { + BoutInitialise(argc, argv); + + Field3D f; + f.setBoundary("f"); + + bout::checkForUnusedOptions(); + + BoutFinalise(); + + // Note: Print message because sometimes MPI ranks error on tidyup. + output.write("SUCCESS"); + + return 0; +} From 88290429ca00f9ad7b03019cca09edc9705af7b7 Mon Sep 17 00:00:00 2001 From: Ben Dudson Date: Tue, 28 Apr 2026 21:27:19 -0700 Subject: [PATCH 2/3] Options::checkForUnusedOptions implement getGlobalUnusedSet Checks for options that are unused on all MPI ranks. Not very efficient but this function isn't called often. --- src/sys/options.cxx | 101 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 2 deletions(-) diff --git a/src/sys/options.cxx b/src/sys/options.cxx index 8bd83c0bfb..95cfb9060a 100644 --- a/src/sys/options.cxx +++ b/src/sys/options.cxx @@ -1137,13 +1137,110 @@ void checkForUnusedOptions() { options["optionfile"].withDefault("BOUT.inp")); } +namespace { +/// Gather the set of unused option keys that are unused on *every* MPI processor. +/// +/// Each processor may use a different subset of options (e.g. because some +/// options are only read on processors handling a particular region). An option +/// should only be considered globally unused — and therefore an error — if it +/// was not used on any processor. +/// +/// Strategy: +/// 1. MPI_Allgather the per-processor serialised key lists so every rank +/// knows the full union of locally-unused keys. +/// 2. For each key in that union, MPI_Allreduce with MPI_PROD over a flag +/// that is 1 if the key is locally unused and 0 if it was used. A +/// product of 1 means every rank left it unused. +/// +/// Keys are serialised as a newline-separated string. Newlines are safe as a +/// separator because option keys use ':' as their only structural character. +std::set getGlobalUnusedSet(std::vector local_unused_keys) { + MPI_Comm comm = BoutComm::get(); + int nprocs{}; + MPI_Comm_size(comm, &nprocs); + + // --- Step 1: share every processor's locally-unused key list --- + + // Serialise this processor's unused keys as a newline-separated string. + // An empty key list produces an empty string, which is handled correctly + // by MPI_Allgatherv (contributing zero bytes). + const std::string local_serialized = + fmt::format("{}", fmt::join(local_unused_keys, "\n")); + int local_len = static_cast(local_serialized.size()); + + // Gather the byte-lengths from all processors so we can allocate the + // receive buffer and build the displacement array for MPI_Allgatherv. + std::vector all_lens(nprocs); + MPI_Allgather(&local_len, 1, MPI_INT, all_lens.data(), 1, MPI_INT, comm); + + std::vector displs(nprocs, 0); + for (int i = 1; i < nprocs; ++i) { + displs[i] = displs[i - 1] + all_lens[i - 1]; + } + const int total_len = displs[nprocs - 1] + all_lens[nprocs - 1]; + + // Gather the serialised key strings from all processors. + std::string all_serialized(total_len, '\0'); + MPI_Allgatherv(local_serialized.data(), local_len, MPI_CHAR, all_serialized.data(), + all_lens.data(), displs.data(), MPI_CHAR, comm); + + // Reconstruct the global union of unused keys by splitting each + // processor's contribution at the newline separator. + std::set global_unused_union; + for (int i = 0; i < nprocs; ++i) { + if (all_lens[i] == 0) { + continue; // processor had no unused keys + } + const std::string proc_keys = all_serialized.substr(displs[i], all_lens[i]); + for (const auto& key : strsplit(proc_keys, '\n')) { + global_unused_union.insert(key); + } + } + + if (global_unused_union.empty()) { + return {}; + } + + // --- Step 2: keep only keys that were unused on *every* processor --- + + // Build a presence flag for each key in the global union: 1 if unused + // locally (i.e. in our local_unused_keys), 0 if it was used here. + const std::set local_set(local_unused_keys.begin(), + local_unused_keys.end()); + const std::vector global_keys(global_unused_union.begin(), + global_unused_union.end()); + + std::vector local_flags(global_keys.size()); + for (std::size_t i = 0; i < global_keys.size(); ++i) { + local_flags[i] = local_set.contains(global_keys[i]) ? 1 : 0; + } + + // MPI_PROD: product across all processors is 1 iff every processor + // contributed 1, i.e. iff the key was unused everywhere. + std::vector global_flags(global_keys.size()); + MPI_Allreduce(local_flags.data(), global_flags.data(), + static_cast(global_keys.size()), MPI_INT, MPI_PROD, comm); + + std::set globally_unused; + for (std::size_t i = 0; i < global_keys.size(); ++i) { + if (global_flags[i] != 0) { + globally_unused.insert(global_keys[i]); + } + } + return globally_unused; +} +} // namespace + void checkForUnusedOptions(const Options& options, const std::string& data_dir, const std::string& option_file) { const Options unused = options.getUnused(); - if (not unused.getChildren().empty()) { + + // Get the keys that are not used on any processor + const auto keys = getGlobalUnusedSet(unused.getFlattenedKeys()); + + if (not keys.empty()) { // Construct a string with all the fuzzy matches for each unused option - const auto keys = unused.getFlattenedKeys(); std::string possible_misspellings; for (const auto& key : keys) { auto fuzzy_matches = options.fuzzyFind(key); From 26f7cc40c2a76e7c4df602c590962f869474b7e8 Mon Sep 17 00:00:00 2001 From: Ben Dudson Date: Wed, 29 Apr 2026 09:32:19 -0700 Subject: [PATCH 3/3] Options getGlobalUnusedSet use BoutComm Co-authored-by: Peter Hill --- src/sys/options.cxx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/sys/options.cxx b/src/sys/options.cxx index 95cfb9060a..85cfa7a49a 100644 --- a/src/sys/options.cxx +++ b/src/sys/options.cxx @@ -1156,8 +1156,7 @@ namespace { /// separator because option keys use ':' as their only structural character. std::set getGlobalUnusedSet(std::vector local_unused_keys) { MPI_Comm comm = BoutComm::get(); - int nprocs{}; - MPI_Comm_size(comm, &nprocs); + const int nprocs = BoutComm::size(); // --- Step 1: share every processor's locally-unused key list ---