From 825c5f4d2bd5dc984de2505506c85726fff9587e Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 2 May 2026 15:33:56 -0600 Subject: [PATCH 01/46] Add rummy as submodule and build it --- .gitmodules | 3 +++ CMakeLists.txt | 18 ++++++++++++++++++ external/rummy | 1 + src/CMakeLists.txt | 15 +++++++++++++++ 4 files changed, 37 insertions(+) create mode 160000 external/rummy diff --git a/.gitmodules b/.gitmodules index bceb2843cd199..d2db6e3bd4062 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "external/kokkos"] path = external/Kokkos url = https://github.com/kokkos/kokkos.git +[submodule "external/rummy"] + path = external/rummy + url = https://github.com/lanl/rummy diff --git a/CMakeLists.txt b/CMakeLists.txt index 2a08ede8d0d55..35edce3a4a015 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -440,6 +440,24 @@ set(CMAKE_INSTALL_INCLUDEDIR "${CMAKE_INSTALL_INCLUDEDIR}/parthenon") set(DOC_GEN_PATH "${CMAKE_SOURCE_DIR}/doc/sphinx/src/generated" CACHE STRING "Path to save generated data for docs.") + +find_package(Rummy QUIET) + +if (NOT Rummy_FOUND) + # If Rummy is not found, instead use the git submodule + if (NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/external/rummy/single_include) + # Unable to find the header files for Rummy or they don't exist + message(STATUS "Downloading Rummy submodule.") + + # Clone the submodule + execute_process(COMMAND git submodule update --init --force -- external/rummy WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) + endif() + + add_subdirectory(external/rummy) + list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/external/rummy/contrib") +endif() + + add_subdirectory(src) add_subdirectory(example) add_subdirectory(benchmarks) diff --git a/external/rummy b/external/rummy new file mode 160000 index 0000000000000..aab9c99f031a0 --- /dev/null +++ b/external/rummy @@ -0,0 +1 @@ +Subproject commit aab9c99f031a08e0526f558d5c1f8fdb99773602 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5f84e8b33bda3..a40b3986859e4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -381,6 +381,8 @@ endif() target_link_libraries(parthenon PUBLIC Kokkos::kokkos Threads::Threads) +target_link_libraries(parthenon PUBLIC Rummy::rummy) + if (PARTHENON_ENABLE_ASCENT) if (ENABLE_MPI) target_link_libraries(parthenon PUBLIC ascent::ascent_mpi) @@ -406,6 +408,19 @@ target_include_directories(parthenon PUBLIC install(TARGETS parthenon EXPORT parthenonTargets) + +if(NOT Rummy_FOUND) + install(TARGETS rummylib pipslib EXPORT parthenonTargets + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + ) + install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../external/rummy/rummy + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + FILES_MATCHING PATTERN "*.hpp" PATTERN "*.h" + ) +endif() + # Maintain directory structure in installed include files install(DIRECTORY ./ TYPE INCLUDE FILES_MATCHING PATTERN "*.hpp") From 89779e05661278fffd74f6baab57833c02a32d69 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 3 May 2026 13:31:20 -0600 Subject: [PATCH 02/46] Update to latest rummy --- external/rummy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/rummy b/external/rummy index aab9c99f031a0..4342260827b4c 160000 --- a/external/rummy +++ b/external/rummy @@ -1 +1 @@ -Subproject commit aab9c99f031a08e0526f558d5c1f8fdb99773602 +Subproject commit 4342260827b4c846eb0c6ef719353d3341ae6fd9 From 56d898d32734bfb8d3278989f6bf1b5e0239ebd6 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 3 May 2026 13:38:43 -0600 Subject: [PATCH 03/46] Add initial Rummy input deck support. This will detect an input file as rummy format and bypass the other input file reader. Includes a fix for Get for length 1 vectors that were read not as vectors --- src/parameter_input.cpp | 208 +++++++++++++++++++++++++++++++++++++- src/parameter_input.hpp | 3 + src/parthenon_manager.cpp | 12 ++- 3 files changed, 215 insertions(+), 8 deletions(-) diff --git a/src/parameter_input.cpp b/src/parameter_input.cpp index 6d0e93d1bc79f..6753082c066e1 100644 --- a/src/parameter_input.cpp +++ b/src/parameter_input.cpp @@ -52,6 +52,9 @@ #include "parameter_input.hpp" +#include "parthenon_mpi.hpp" +#include "rummy/deck.hpp" + #include #include #include @@ -78,10 +81,14 @@ namespace parthenon { ParameterInput::ParameterInput() : last_filename_{} {} ParameterInput::ParameterInput(std::string input_filename) : last_filename_{} { - IOWrapper infile; - infile.Open(input_filename.c_str(), IOWrapper::FileMode::read); - LoadFromFile(infile); - infile.Close(); + if (IsRummyFormat(input_filename)) { + LoadFromRummyFile(input_filename); + } else { + IOWrapper infile; + infile.Open(input_filename.c_str(), IOWrapper::FileMode::read); + LoadFromFile(infile); + infile.Close(); + } } ParameterInput::~ParameterInput() = default; @@ -232,6 +239,186 @@ void ParameterInput::LoadFromFile(IOWrapper &input) { return; } +//---------------------------------------------------------------------------------------- +//! \fn bool ParameterInput::IsRummyFormat(const std::string &filename) +// \brief Detect whether a file uses Rummy input format by scanning for markers: +// - First line is "# use rummy" (case-insensitive) +// - Non-comment, non-blank content before the first line +// - Relative suit paths starting with <.. +// - Rummy-specific value syntax: ** power operator, quoted strings, +// bracket syntax [ ] (vectors/slices), or slice colon inside brackets +bool ParameterInput::IsRummyFormat(const std::string &filename) { + std::ifstream file(filename); + if (!file.is_open()) return false; + + bool first_line = true; + bool found_block = false; + std::string line; + while (std::getline(file, line)) { + line.erase(std::remove_if(line.begin(), line.end(), + [](char c) { return std::isspace(c) && c != ' '; }), + line.end()); + if (line.empty()) continue; + auto first_char = line.find_first_not_of(" "); + if (first_char == std::string::npos) continue; + + // Check first non-blank line for "# use rummy" (case-insensitive) + if (first_line) { + first_line = false; + if (line.compare(first_char, 1, "#") == 0) { + std::string after_hash = line.substr(first_char + 1); + auto text_start = after_hash.find_first_not_of(" "); + if (text_start != std::string::npos) { + std::string token = after_hash.substr(text_start); + std::transform(token.begin(), token.end(), token.begin(), ::tolower); + if (token.compare(0, 9, "use rummy") == 0) return true; + } + continue; + } + } else { + if (line.compare(first_char, 1, "#") == 0) continue; + } + + if (line.compare(first_char, 1, "<") == 0) { + if (line.size() > first_char + 2 && line.compare(first_char + 1, 2, "..") == 0) + return true; + found_block = true; + continue; + } + + // Non-comment, non-blank content before the first block = Rummy global variable + if (!found_block) return true; + + // Rummy-specific syntax in the value part + auto eq_pos = line.find('='); + if (eq_pos != std::string::npos) { + std::string value_part = line.substr(eq_pos + 1); + if (value_part.find("**") != std::string::npos) return true; // power operator + if (value_part.find('"') != std::string::npos) return true; // quoted string + if (value_part.find('[') != std::string::npos) return true; // vector/slice syntax + } + // Slice syntax on the LHS: name[:2] or name[0:2] + std::string lhs = line.substr(first_char, eq_pos == std::string::npos + ? std::string::npos + : eq_pos - first_char); + if (lhs.find('[') != std::string::npos) return true; + } + return false; +} + +//---------------------------------------------------------------------------------------- +// Helper functions local to this translation unit for Rummy card conversion + +namespace { + +//! \fn ParameterInput::ParamValue ConvertRummyCard(const Rummy::Card &card) +// \brief Convert a Rummy Card to a ParameterInput::ParamValue for storage in ParameterInput. +ParameterInput::ParamValue ConvertRummyCard(const Rummy::Card &card) { + if (card.isBool()) { + return card.Get(); + } else if (card.isString()) { + return card.Get(); + } else { + // Otherwise store as UnresolvedString to preserve full precision + return ParameterInput::UnresolvedString( + card.GetString(std::numeric_limits::max_digits10)); + } +} + +//! \fn std::string RummyCardToString(const Rummy::Card &card) +// \brief Convert a Rummy Card to its string representation. +std::string RummyCardToString(const Rummy::Card &card) { + if (card.isBool()) { + return card.Get() ? "true" : "false"; + } + return card.GetString(std::numeric_limits::max_digits10); +} + +} // anonymous namespace + +//---------------------------------------------------------------------------------------- +//! \fn void ParameterInput::LoadFromRummyStream(std::istream &is) +// \brief Load parameters from a Rummy-format stream into ParameterInput storage. +void ParameterInput::LoadFromRummyStream(std::istream &is) { + PARTHENON_REQUIRE_THROWS(!parsing_finalized_, + "Can't add new parameters after parsing is resolved."); + + Rummy::Deck deck; + deck.Build(is); + + static const std::regex kVectorCardPattern(R"(^(.+)\[(\d+)\]$)"); + + for (const auto &suit_name : deck.GetSuitsInOrder()) { + const std::string &block_name = suit_name; + const auto &suit_cards = deck.GetCardsInOrder(suit_name); + for (const auto &card_name : suit_cards) { + // match for vector + if (deck.IsCardVector(suit_name, card_name)) { + std::vector comments; + auto elements = deck.GetVector(suit_name, card_name, comments); + std::string joined; + std::string joined_comments; + for (std::size_t i = 0; i < elements.size(); ++i) { + if (comments[i] != "") { + if (i > 0) { + joined_comments += " "; + } + joined_comments += comments[i]; + } + if (i > 0) { + joined += ","; + } + joined += elements[i]; + } + AddParsedParameter(block_name, card_name, UnresolvedString(joined), + joined_comments); + } else { + auto &card = deck.GetCard(suit_name, card_name); + AddParsedParameter(block_name, card_name, ConvertRummyCard(card), card.GetComment()); + } + } + } +} + +//---------------------------------------------------------------------------------------- +//! \fn void ParameterInput::LoadFromRummyFile(const std::string &filename) +// \brief MPI-safe loader for Rummy-format input files. +void ParameterInput::LoadFromRummyFile(const std::string &filename) { + PARTHENON_REQUIRE_THROWS(!parsing_finalized_, + "Can't add new parameters after parsing is resolved."); + + std::string content; + +#ifdef MPI_PARALLEL + std::size_t content_size = 0; + if (Globals::my_rank == 0) { + std::ifstream file(filename); + PARTHENON_REQUIRE_THROWS(file.is_open(), + "Could not open Rummy input file: " + filename); + std::ostringstream oss; + oss << file.rdbuf(); + content = oss.str(); + content_size = content.size(); + } + PARTHENON_MPI_CHECK( + MPI_Bcast(&content_size, sizeof(std::size_t), MPI_BYTE, 0, MPI_COMM_WORLD)); + content.resize(content_size); + PARTHENON_MPI_CHECK( + MPI_Bcast(content.data(), static_cast(content_size), MPI_BYTE, 0, + MPI_COMM_WORLD)); +#else + std::ifstream file(filename); + PARTHENON_REQUIRE_THROWS(file.is_open(), + "Could not open Rummy input file: " + filename); + std::ostringstream oss; + oss << file.rdbuf(); + content = oss.str(); +#endif + + std::istringstream is(content); + LoadFromRummyStream(is); +} + //---------------------------------------------------------------------------------------- //! \fn Block* ParameterInput::FindBlock_(const std::string & name) // \brief find specified Block. Returns pointer to block or nullptr. @@ -959,6 +1146,19 @@ std::optional ParameterInput::GetFromStorage_(const std::string &block, return std::get(param->value); } + // If T is a vector and the stored value is the scalar element type, wrap it. + // This handles the case where a single-element vector was stored as a scalar + // (e.g. a one-element string vector stored as std::string). + if constexpr (std::is_same_v> || + std::is_same_v> || + std::is_same_v> || + std::is_same_v>) { + using ElemType = typename T::value_type; + if (std::holds_alternative(param->value)) { + return T{std::get(param->value)}; + } + } + // Type mismatch - was previously resolved as a different type std::stringstream msg; msg << "### FATAL ERROR in ParameterInput::GetFromStorage_" << std::endl diff --git a/src/parameter_input.hpp b/src/parameter_input.hpp index e94e67b1d329f..4715fe30168d5 100644 --- a/src/parameter_input.hpp +++ b/src/parameter_input.hpp @@ -219,6 +219,9 @@ class ParameterInput { // === PARSING INTERFACE === void LoadFromStream(std::istream &is); void LoadFromFile(IOWrapper &input); + void LoadFromRummyFile(const std::string &filename); + void LoadFromRummyStream(std::istream &is); + static bool IsRummyFormat(const std::string &filename); void ModifyFromCmdline(int argc, char *argv[]); // === PARSER INTERFACE (for input sources like text files, Python, TOML, etc.) === diff --git a/src/parthenon_manager.cpp b/src/parthenon_manager.cpp index 5b314b9c9f4f0..51b4d19bf58a9 100644 --- a/src/parthenon_manager.cpp +++ b/src/parthenon_manager.cpp @@ -123,10 +123,14 @@ ParthenonStatus ParthenonManager::ParthenonInitEnv(int argc, char *argv[]) { if (arg.input_filename != nullptr) { // Modify info read from restart file if (arg.is_restart) { - IOWrapper infile; - infile.Open(arg.input_filename, IOWrapper::FileMode::read); - pinput->LoadFromFile(infile); - infile.Close(); + if (ParameterInput::IsRummyFormat(arg.input_filename)) { + pinput->LoadFromRummyFile(arg.input_filename); + } else { + IOWrapper infile; + infile.Open(arg.input_filename, IOWrapper::FileMode::read); + pinput->LoadFromFile(infile); + infile.Close(); + } // Populate new object for fresh simulation } else { From 92ab333ffaa3e996692100870d76c34aefd0560c Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 3 May 2026 13:39:20 -0600 Subject: [PATCH 04/46] Add a set of unit tests for Rummy inputs --- tst/unit/CMakeLists.txt | 1 + tst/unit/test_rummy.cpp | 427 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 428 insertions(+) create mode 100644 tst/unit/test_rummy.cpp diff --git a/tst/unit/CMakeLists.txt b/tst/unit/CMakeLists.txt index 84a88a5a9eb9b..24e6d6458e138 100644 --- a/tst/unit/CMakeLists.txt +++ b/tst/unit/CMakeLists.txt @@ -39,6 +39,7 @@ list(APPEND unit_tests_SOURCES test_pararrays.cpp test_sparse_pack.cpp test_parameter_input.cpp + test_rummy.cpp test_error_checking.cpp test_object_pool.cpp test_partitioning.cpp diff --git a/tst/unit/test_rummy.cpp b/tst/unit/test_rummy.cpp new file mode 100644 index 0000000000000..2d920c67da683 --- /dev/null +++ b/tst/unit/test_rummy.cpp @@ -0,0 +1,427 @@ +//======================================================================================== +// (C) (or copyright) 2020-2026. Triad National Security, LLC. All rights reserved. +// +// This program was produced under U.S. Government contract 89233218CNA000001 for Los +// Alamos National Laboratory (LANL), which is operated by Triad National Security, LLC +// for the U.S. Department of Energy/National Nuclear Security Administration. All rights +// in the program are reserved by Triad National Security, LLC, and the U.S. Department +// of Energy/National Nuclear Security Administration. The Government is granted for +// itself and others acting on its behalf a nonexclusive, paid-up, irrevocable worldwide +// license in this material to reproduce, prepare derivative works, distribute copies to +// the public, perform publicly and display publicly, and to permit others to do so. +//======================================================================================== + +#include +#include +#include +#include +#include + +#include + +#include "parameter_input.hpp" + +using parthenon::ParameterInput; + +TEST_CASE("LoadFromRummyStream: basic scalar types", "[Rummy]") { + GIVEN("A Rummy-format stream with bool, string, and numeric cards") { + ParameterInput in; + std::istringstream ss( + "\n" + "nx = 64\n" + "cfl = 0.4\n" + "active = true\n" + "label = \"hydro\"\n"); + in.LoadFromRummyStream(ss); + + THEN("Integer parameter is readable") { + REQUIRE(in.GetInteger("mesh", "nx") == 64); + } + THEN("Real parameter is readable") { + REQUIRE(in.GetReal("mesh", "cfl") == Approx(0.4)); + } + THEN("Boolean parameter is readable") { + REQUIRE(in.GetBoolean("mesh", "active") == true); + } + THEN("String parameter is readable") { + REQUIRE(in.GetString("mesh", "label") == "hydro"); + } + THEN("Block exists") { + REQUIRE(in.DoesBlockExist("mesh")); + } + THEN("Parameters exist") { + REQUIRE(in.DoesParameterExist("mesh", "nx")); + REQUIRE(in.DoesParameterExist("mesh", "cfl")); + } + } +} + +TEST_CASE("LoadFromRummyStream: global variables go to '/' block", "[Rummy]") { + GIVEN("A Rummy-format stream with global variables") { + ParameterInput in; + std::istringstream ss( + "Lx = 1.0\n" + "flag = false\n" + "name = \"global_scope\"\n" + "\n" + "nx = 10\n"); + in.LoadFromRummyStream(ss); + + THEN("Globals are stored under the '/' block") { + REQUIRE(in.DoesParameterExist("/", "Lx")); + REQUIRE(in.GetReal("/", "Lx") == Approx(1.0)); + REQUIRE(in.DoesParameterExist("/", "flag")); + REQUIRE(in.GetBoolean("/", "flag") == false); + REQUIRE(in.GetString("/", "name") == "global_scope"); + } + THEN("Non-global parameters are unaffected") { + REQUIRE(in.GetInteger("mesh", "nx") == 10); + } + } +} + +TEST_CASE("LoadFromRummyStream: numeric vector reconstruction", "[Rummy]") { + GIVEN("A Rummy stream with a vector of reals and a vector of ints") { + ParameterInput in; + std::istringstream ss( + "\n" + "vals = [1.5, 2.5, 3.5]\n" + "counts = [10, 20, 30]\n"); + in.LoadFromRummyStream(ss); + + THEN("Real vector is reconstructed correctly") { + auto v = in.GetVector("block", "vals"); + REQUIRE(v.size() == 3); + REQUIRE(v[0] == Approx(1.5)); + REQUIRE(v[1] == Approx(2.5)); + REQUIRE(v[2] == Approx(3.5)); + } + THEN("Integer vector is reconstructed correctly") { + auto v = in.GetVector("block", "counts"); + REQUIRE(v.size() == 3); + REQUIRE(v[0] == 10); + REQUIRE(v[1] == 20); + REQUIRE(v[2] == 30); + } + } +} + +TEST_CASE("LoadFromRummyStream: string vector reconstruction", "[Rummy]") { + GIVEN("A Rummy stream with a vector of strings") { + ParameterInput in; + std::istringstream ss( + "\n" + "tags = [\"alpha\", \"beta\", \"gamma\"]\n"); + in.LoadFromRummyStream(ss); + + THEN("String vector is reconstructed correctly") { + auto v = in.GetVector("block", "tags"); + REQUIRE(v.size() == 3); + REQUIRE(v[0] == "alpha"); + REQUIRE(v[1] == "beta"); + REQUIRE(v[2] == "gamma"); + } + } +} + +TEST_CASE("LoadFromRummyStream: expressions are evaluated", "[Rummy]") { + GIVEN("A Rummy stream with arithmetic expressions and cross-suit references") { + ParameterInput in; + std::istringstream ss( + "base = 4.0\n" + "\n" + "doubled = base * 2.0\n" + "squared = base**2\n"); + in.LoadFromRummyStream(ss); + + THEN("Expressions are fully evaluated before storage") { + REQUIRE(in.GetReal("block", "doubled") == Approx(8.0)); + REQUIRE(in.GetReal("block", "squared") == Approx(16.0)); + } + } +} + +TEST_CASE("IsRummyFormat: detects Rummy vs legacy format", "[Rummy]") { + GIVEN("A legacy-format input file (block header before any value)") { + std::string tmpfile = "/tmp/parthenon_test_legacy.pin"; + { + std::ofstream f(tmpfile); + f << "\n" + << "nx1 = 64\n" + << "nx2 = 32\n"; + } + THEN("IsRummyFormat returns false") { + REQUIRE(ParameterInput::IsRummyFormat(tmpfile) == false); + } + } + + GIVEN("A Rummy-format file: global variable before first block") { + std::string tmpfile = "/tmp/parthenon_test_rummy_global.pin"; + { + std::ofstream f(tmpfile); + f << "Lx = 1.0\n" + << "\n" + << "nx = 64\n"; + } + THEN("IsRummyFormat returns true") { + REQUIRE(ParameterInput::IsRummyFormat(tmpfile) == true); + } + } + + GIVEN("A Rummy-format file: relative suit path <../") { + std::string tmpfile = "/tmp/parthenon_test_rummy_relpath.pin"; + { + std::ofstream f(tmpfile); + f << "\n" + << "hydro = true\n" + << "<../eos>\n" + << "gamma = 1.4\n"; + } + THEN("IsRummyFormat returns true") { + REQUIRE(ParameterInput::IsRummyFormat(tmpfile) == true); + } + } + + GIVEN("A Rummy-format file: ** power operator in a value") { + std::string tmpfile = "/tmp/parthenon_test_rummy_power.pin"; + { + std::ofstream f(tmpfile); + f << "\n" + << "val = 2**10\n"; + } + THEN("IsRummyFormat returns true") { + REQUIRE(ParameterInput::IsRummyFormat(tmpfile) == true); + } + } + + GIVEN("A Rummy-format file: first line is '# use rummy'") { + std::string tmpfile = "/tmp/parthenon_test_rummy_userummy.pin"; + { + std::ofstream f(tmpfile); + f << "# Use Rummy\n" + << "\n" + << "nx = 64\n"; + } + THEN("IsRummyFormat returns true") { + REQUIRE(ParameterInput::IsRummyFormat(tmpfile) == true); + } + } + + GIVEN("A Rummy-format file: quoted string value") { + std::string tmpfile = "/tmp/parthenon_test_rummy_quoted.pin"; + { + std::ofstream f(tmpfile); + f << "\n" + << "label = \"hydro\"\n"; + } + THEN("IsRummyFormat returns true") { + REQUIRE(ParameterInput::IsRummyFormat(tmpfile) == true); + } + } + + GIVEN("A Rummy-format file: bracket vector syntax in a value") { + std::string tmpfile = "/tmp/parthenon_test_rummy_vec.pin"; + { + std::ofstream f(tmpfile); + f << "\n" + << "nx = [64, 32, 16]\n"; + } + THEN("IsRummyFormat returns true") { + REQUIRE(ParameterInput::IsRummyFormat(tmpfile) == true); + } + } + + GIVEN("A Rummy-format file: bracket slice syntax on the LHS") { + std::string tmpfile = "/tmp/parthenon_test_rummy_slice.pin"; + { + std::ofstream f(tmpfile); + f << "\n" + << "nx[:2] = [64, 32]\n"; + } + THEN("IsRummyFormat returns true") { + REQUIRE(ParameterInput::IsRummyFormat(tmpfile) == true); + } + } +} + +TEST_CASE("LoadFromRummyStream: ModifyFromCmdline overrides Rummy params", "[Rummy]") { + GIVEN("A Rummy stream with a parameter") { + ParameterInput in; + std::istringstream ss("\nnx = 32\n"); + in.LoadFromRummyStream(ss); + + WHEN("ModifyFromCmdline overrides the parameter") { + const char *argv[] = {"program", "mesh/nx=128"}; + in.ModifyFromCmdline(2, const_cast(argv)); + + THEN("The override wins") { + REQUIRE(in.GetInteger("mesh", "nx") == 128); + } + } + } +} + +TEST_CASE("LoadFromRummyStream: comma-separated vector without brackets", "[Rummy]") { + GIVEN("A Rummy stream using bare comma-separated syntax") { + ParameterInput in; + std::istringstream ss( + "\n" + "vals = 1.0, 2.0, 3.0\n" + "counts = 10, 20, 30\n"); + in.LoadFromRummyStream(ss); + + THEN("Real vector is reconstructed correctly") { + auto v = in.GetVector("block", "vals"); + REQUIRE(v.size() == 3); + REQUIRE(v[0] == Approx(1.0)); + REQUIRE(v[1] == Approx(2.0)); + REQUIRE(v[2] == Approx(3.0)); + } + THEN("Integer vector is reconstructed correctly") { + auto v = in.GetVector("block", "counts"); + REQUIRE(v.size() == 3); + REQUIRE(v[0] == 10); + REQUIRE(v[1] == 20); + REQUIRE(v[2] == 30); + } + } +} + +TEST_CASE("LoadFromRummyStream: slice assignment syntax", "[Rummy]") { + GIVEN("A Rummy stream using slice assignment v[:N] = [...]") { + ParameterInput in; + std::istringstream ss( + "\n" + "v[:3] = [100, 200, 300]\n"); + in.LoadFromRummyStream(ss); + + THEN("Vector is reconstructed correctly from slice assignment") { + auto v = in.GetVector("block", "v"); + REQUIRE(v.size() == 3); + REQUIRE(v[0] == 100); + REQUIRE(v[1] == 200); + REQUIRE(v[2] == 300); + } + } +} + +TEST_CASE("LoadFromRummyStream: cross-block references are evaluated", "[Rummy]") { + GIVEN("A Rummy stream where one block references another block's variable") { + ParameterInput in; + std::istringstream ss( + "\n" + "gamma = 1.4\n" + "\n" + "gamma_minus_one = physics.gamma - 1.0\n" + "gamma_sq = physics.gamma ** 2\n"); + in.LoadFromRummyStream(ss); + + THEN("Cross-block reference is fully evaluated before storage") { + REQUIRE(in.GetReal("eos", "gamma_minus_one") == Approx(0.4)); + REQUIRE(in.GetReal("eos", "gamma_sq") == Approx(1.96)); + } + } +} + +TEST_CASE("LoadFromRummyStream: global variables accessible from blocks", "[Rummy]") { + GIVEN("A Rummy stream with a global variable used inside a block") { + ParameterInput in; + std::istringstream ss( + "Lx = 10.0\n" + "\n" + "dx = Lx / 100\n" + "half_Lx = Lx * 0.5\n"); + in.LoadFromRummyStream(ss); + + THEN("Global is stored under the '/' block") { + REQUIRE(in.GetReal("/", "Lx") == Approx(10.0)); + } + THEN("Block parameters referencing the global are evaluated") { + REQUIRE(in.GetReal("mesh", "dx") == Approx(0.1)); + REQUIRE(in.GetReal("mesh", "half_Lx") == Approx(5.0)); + } + } +} + +TEST_CASE("LoadFromRummyStream: print statement outside a block", "[Rummy]") { + GIVEN("A Rummy stream with a print statement before any block") { + ParameterInput in; + // print is a Rummy/pips statement; it produces output but no card. + // Verify it doesn't crash and doesn't appear as a parameter. + std::istringstream ss( + "x = 42.0\n" + "print(x)\n" + "\n" + "y = x + 1\n"); + + THEN("LoadFromRummyStream completes without error") { + REQUIRE_NOTHROW(in.LoadFromRummyStream(ss)); + } + AND_THEN("The print statement produces no stored parameter") { + std::istringstream ss2( + "x = 42.0\n" + "print(x)\n" + "\n" + "y = x + 1\n"); + in.LoadFromRummyStream(ss2); + REQUIRE_FALSE(in.DoesParameterExist("/", "print")); + REQUIRE(in.GetReal("block", "y") == Approx(43.0)); + } + } +} + +TEST_CASE("LoadFromRummyStream: vector slice with element-wise math", "[Rummy]") { + GIVEN("A Rummy stream that defines a 3-element vector, then cubes a 2-element sub-slice") { + ParameterInput in; + // base[:3] defines [2.0, 3.0, 4.0]. + // cubed[:2] = base[:2] ** 3 takes only the first two elements and cubes them. + std::istringstream ss( + "\n" + "base[:3] = [2.0, 3.0, 4.0]\n" + "cubed[:2] = base[:2] ** 3\n"); + in.LoadFromRummyStream(ss); + + THEN("Base vector retains all three elements") { + auto b = in.GetVector("block", "base"); + REQUIRE(b.size() == 3); + REQUIRE(b[0] == Approx(2.0)); + REQUIRE(b[1] == Approx(3.0)); + REQUIRE(b[2] == Approx(4.0)); + } + THEN("Cubed slice contains only the first two elements, each cubed") { + auto c = in.GetVector("block", "cubed"); + REQUIRE(c.size() == 2); + REQUIRE(c[0] == Approx(8.0)); // 2^3 + REQUIRE(c[1] == Approx(27.0)); // 3^3 + } + } +} + +TEST_CASE("LoadFromRummyStream: second stream overwrites existing parameters", "[Rummy]") { + GIVEN("A first Rummy stream establishing initial values") { + ParameterInput in; + std::istringstream ss1( + "\n" + "nx = 64\n" + "cfl = 0.3\n" + "\n" + "gamma = 1.4\n"); + in.LoadFromRummyStream(ss1); + + WHEN("A second Rummy stream updates some of those parameters") { + std::istringstream ss2( + "\n" + "nx = 128\n" + "cfl = 0.5\n"); + in.LoadFromRummyStream(ss2); + + THEN("Updated parameters reflect the second stream") { + REQUIRE(in.GetInteger("mesh", "nx") == 128); + REQUIRE(in.GetReal("mesh", "cfl") == Approx(0.5)); + } + THEN("Parameters not present in the second stream are unchanged") { + REQUIRE(in.GetReal("physics", "gamma") == Approx(1.4)); + } + } + } +} From 2c3454c8983541daf835e4864879b7ecb25d3d41 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 3 May 2026 14:19:38 -0600 Subject: [PATCH 05/46] Update rummy --- external/rummy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/rummy b/external/rummy index 4342260827b4c..b9d70ffcc074e 160000 --- a/external/rummy +++ b/external/rummy @@ -1 +1 @@ -Subproject commit 4342260827b4c846eb0c6ef719353d3341ae6fd9 +Subproject commit b9d70ffcc074e23c8199d8f6b25fa92e83a34b4b From 61c201e847800f9c76db01652387ad4800cb3476 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 3 May 2026 17:05:32 -0600 Subject: [PATCH 06/46] Update Rummy --- external/rummy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/rummy b/external/rummy index b9d70ffcc074e..83a83cac685e3 160000 --- a/external/rummy +++ b/external/rummy @@ -1 +1 @@ -Subproject commit b9d70ffcc074e23c8199d8f6b25fa92e83a34b4b +Subproject commit 83a83cac685e34d690b549463e5d7df8c5687dfb From 8c928e4697c4fe3662b89d00017c3eacdf33b6f3 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 3 May 2026 17:25:41 -0600 Subject: [PATCH 07/46] Update Rummy again --- external/rummy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/rummy b/external/rummy index 83a83cac685e3..1733be5468705 160000 --- a/external/rummy +++ b/external/rummy @@ -1 +1 @@ -Subproject commit 83a83cac685e34d690b549463e5d7df8c5687dfb +Subproject commit 1733be5468705ba826d2a8c5ba2da5f95c2ef7d9 From 637481b5c895e58100bea58b8d86b9c81c65e83c Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 3 May 2026 17:56:29 -0600 Subject: [PATCH 08/46] Update Rummy again --- external/rummy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/rummy b/external/rummy index 1733be5468705..0725950493988 160000 --- a/external/rummy +++ b/external/rummy @@ -1 +1 @@ -Subproject commit 1733be5468705ba826d2a8c5ba2da5f95c2ef7d9 +Subproject commit 07259504939887ab310df80fec296ab85c0ed1b9 From 708fb88740db05f43f2fe342fd670bcde1114b95 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 3 May 2026 17:57:48 -0600 Subject: [PATCH 09/46] Support for multiple input decks and introduces the InputFormat enum. Store the rummy deck in parameterinput now --- src/argument_parser.hpp | 11 +++++++---- src/parameter_input.cpp | 29 +++++++++++++++++++++++++---- src/parameter_input.hpp | 12 +++++++++++- src/parthenon_manager.cpp | 23 ++++++++++------------- 4 files changed, 53 insertions(+), 22 deletions(-) diff --git a/src/argument_parser.hpp b/src/argument_parser.hpp index 134a48ea7fa84..5034112bae157 100644 --- a/src/argument_parser.hpp +++ b/src/argument_parser.hpp @@ -51,7 +51,7 @@ class ArgParse { switch (opt_letter) { case 'i': // -i invalid = invalid_arg(); - input_filename = argv[++i]; + input_filenames.push_back(argv[++i]); break; case 'r': // -r invalid = invalid_arg(); @@ -128,10 +128,12 @@ class ArgParse { } return ArgStatus::error; } - } // else if argv[i] not of form "-?" ignore it here (tested in ModifyFromCmdline) + } else { + modifiers.push_back(argv[i]); + } } - if (restart_filename == nullptr && input_filename == nullptr) { + if (restart_filename == nullptr && input_filenames.empty()) { // no input file is given std::cout << "### FATAL ERROR in main" << std::endl << "No input file or restart file is specified." << std::endl; @@ -140,7 +142,8 @@ class ArgParse { return ArgStatus::ok; } - char *input_filename = nullptr; + std::vector input_filenames; + std::vector modifiers; char *restart_filename = nullptr; char *prundir = nullptr; char *params_regex = nullptr; diff --git a/src/parameter_input.cpp b/src/parameter_input.cpp index 6753082c066e1..e1ac2e5e4691c 100644 --- a/src/parameter_input.cpp +++ b/src/parameter_input.cpp @@ -81,13 +81,21 @@ namespace parthenon { ParameterInput::ParameterInput() : last_filename_{} {} ParameterInput::ParameterInput(std::string input_filename) : last_filename_{} { + ReadFile(input_filename); +} + +void ParameterInput::ReadFile(const std::string &input_filename) { if (IsRummyFormat(input_filename)) { LoadFromRummyFile(input_filename); + format = InputFormat::Rummy; } else { IOWrapper infile; infile.Open(input_filename.c_str(), IOWrapper::FileMode::read); LoadFromFile(infile); infile.Close(); + if (format != InputFormat::Rummy) { + format = InputFormat::Native; + } } } @@ -343,7 +351,6 @@ void ParameterInput::LoadFromRummyStream(std::istream &is) { PARTHENON_REQUIRE_THROWS(!parsing_finalized_, "Can't add new parameters after parsing is resolved."); - Rummy::Deck deck; deck.Build(is); static const std::regex kVectorCardPattern(R"(^(.+)\[(\d+)\]$)"); @@ -516,15 +523,29 @@ bool ParameterInput::ParseLine(std::string line, std::string &name, std::string // \brief parse commandline for changes to input parameters // Note this function is very forgiving (no warnings!) if there is an error in format -void ParameterInput::ModifyFromCmdline(int argc, char *argv[]) { +void ParameterInput::ModifyFromCmdline(std::vector mods) { PARTHENON_REQUIRE_THROWS( !parsing_finalized_, "Can't add new parameters to the linked list after the map is resolved."); + + if (mods.empty()) return; + PARTHENON_REQUIRE_THROWS( + format != InputFormat::Unknown, + "Can't determine the input format."); + if (format == InputFormat::Rummy) { + std::stringstream ss; + for (const auto &mod : mods) { + ss << mod << "\n"; + } + LoadFromRummyStream(ss); + return; + } + + // Native parsing std::string input_text, block, name, value; std::stringstream msg; - for (int i = 1; i < argc; i++) { - input_text = argv[i]; + for (const auto &input_text : mods) { std::size_t equal_posn = input_text.find_first_of("="); // first "=" character std::size_t slash_posn = input_text.rfind("/", equal_posn); // last "/" before "=" diff --git a/src/parameter_input.hpp b/src/parameter_input.hpp index 4715fe30168d5..47877e1a98414 100644 --- a/src/parameter_input.hpp +++ b/src/parameter_input.hpp @@ -46,10 +46,13 @@ #include "utils/sort.hpp" #include "utils/string_utils.hpp" #include "utils/type_list.hpp" +#include "rummy/deck.hpp" #include "utils/utils.hpp" namespace parthenon { +enum class InputFormat { Native, Rummy, Unknown }; + //---------------------------------------------------------------------------------------- // Supported parameter types - single source of truth //---------------------------------------------------------------------------------------- @@ -215,6 +218,7 @@ class ParameterInput { ParameterInput(); explicit ParameterInput(std::string input_filename); ~ParameterInput(); + void ReadFile(const std::string &input_filename); // === PARSING INTERFACE === void LoadFromStream(std::istream &is); @@ -222,7 +226,7 @@ class ParameterInput { void LoadFromRummyFile(const std::string &filename); void LoadFromRummyStream(std::istream &is); static bool IsRummyFormat(const std::string &filename); - void ModifyFromCmdline(int argc, char *argv[]); + void ModifyFromCmdline(std::vector mods); // === PARSER INTERFACE (for input sources like text files, Python, TOML, etc.) === // Use AddParsedParameter to populate parameters from external input sources @@ -435,7 +439,13 @@ class ParameterInput { return ret; } + void SetFormat(InputFormat fmt) { format = fmt; } + InputFormat GetFormat() const { return format; } + private: + + InputFormat format = InputFormat::Unknown; + Rummy::Deck deck; // === PARAMETER STORAGE (vector-of-vectors, preserves insertion order) === std::vector param_storage_; // Ordered storage (for iteration) std::unordered_map diff --git a/src/parthenon_manager.cpp b/src/parthenon_manager.cpp index 51b4d19bf58a9..c381588bb14c5 100644 --- a/src/parthenon_manager.cpp +++ b/src/parthenon_manager.cpp @@ -120,27 +120,24 @@ ParthenonStatus ParthenonManager::ParthenonInitEnv(int argc, char *argv[]) { pinput->LoadFromStream(is); } // If an input file was provided - if (arg.input_filename != nullptr) { + if (!arg.input_filenames.empty()) { // Modify info read from restart file if (arg.is_restart) { - if (ParameterInput::IsRummyFormat(arg.input_filename)) { - pinput->LoadFromRummyFile(arg.input_filename); - } else { - IOWrapper infile; - infile.Open(arg.input_filename, IOWrapper::FileMode::read); - pinput->LoadFromFile(infile); - infile.Close(); + for(const auto &input_filename : arg.input_filenames) { + pinput->ReadFile(input_filename); } - // Populate new object for fresh simulation } else { - pinput = std::make_unique(arg.input_filename); + pinput = std::make_unique(); + for (const auto &input_filename : arg.input_filenames) { + pinput->ReadFile(input_filename); + } } } - + // Modify based on command line inputs - pinput->ModifyFromCmdline(argc, argv); - + pinput->ModifyFromCmdline(arg.modifiers); + // Finalize parsing phase - parsers can no longer add parameters pinput->FinalizeParsing(); From 3895bdb1e4c72d3461f329392128551fc00c72eb Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 3 May 2026 17:58:01 -0600 Subject: [PATCH 10/46] Set the format in the rummy tests --- tst/unit/test_rummy.cpp | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tst/unit/test_rummy.cpp b/tst/unit/test_rummy.cpp index 2d920c67da683..963046c9572cf 100644 --- a/tst/unit/test_rummy.cpp +++ b/tst/unit/test_rummy.cpp @@ -26,6 +26,7 @@ using parthenon::ParameterInput; TEST_CASE("LoadFromRummyStream: basic scalar types", "[Rummy]") { GIVEN("A Rummy-format stream with bool, string, and numeric cards") { ParameterInput in; + in.SetFormat(parthenon::InputFormat::Rummy); std::istringstream ss( "\n" "nx = 64\n" @@ -59,6 +60,7 @@ TEST_CASE("LoadFromRummyStream: basic scalar types", "[Rummy]") { TEST_CASE("LoadFromRummyStream: global variables go to '/' block", "[Rummy]") { GIVEN("A Rummy-format stream with global variables") { ParameterInput in; + in.SetFormat(parthenon::InputFormat::Rummy); std::istringstream ss( "Lx = 1.0\n" "flag = false\n" @@ -83,6 +85,8 @@ TEST_CASE("LoadFromRummyStream: global variables go to '/' block", "[Rummy]") { TEST_CASE("LoadFromRummyStream: numeric vector reconstruction", "[Rummy]") { GIVEN("A Rummy stream with a vector of reals and a vector of ints") { ParameterInput in; + in.SetFormat(parthenon::InputFormat::Rummy); + std::istringstream ss( "\n" "vals = [1.5, 2.5, 3.5]\n" @@ -109,6 +113,7 @@ TEST_CASE("LoadFromRummyStream: numeric vector reconstruction", "[Rummy]") { TEST_CASE("LoadFromRummyStream: string vector reconstruction", "[Rummy]") { GIVEN("A Rummy stream with a vector of strings") { ParameterInput in; + in.SetFormat(parthenon::InputFormat::Rummy); std::istringstream ss( "\n" "tags = [\"alpha\", \"beta\", \"gamma\"]\n"); @@ -127,6 +132,7 @@ TEST_CASE("LoadFromRummyStream: string vector reconstruction", "[Rummy]") { TEST_CASE("LoadFromRummyStream: expressions are evaluated", "[Rummy]") { GIVEN("A Rummy stream with arithmetic expressions and cross-suit references") { ParameterInput in; + in.SetFormat(parthenon::InputFormat::Rummy); std::istringstream ss( "base = 4.0\n" "\n" @@ -247,12 +253,12 @@ TEST_CASE("IsRummyFormat: detects Rummy vs legacy format", "[Rummy]") { TEST_CASE("LoadFromRummyStream: ModifyFromCmdline overrides Rummy params", "[Rummy]") { GIVEN("A Rummy stream with a parameter") { ParameterInput in; + in.SetFormat(parthenon::InputFormat::Rummy); std::istringstream ss("\nnx = 32\n"); in.LoadFromRummyStream(ss); - WHEN("ModifyFromCmdline overrides the parameter") { - const char *argv[] = {"program", "mesh/nx=128"}; - in.ModifyFromCmdline(2, const_cast(argv)); + WHEN("ModifyFromCmdline overrides the parameter") { + in.ModifyFromCmdline({"mesh.nx = 128"}); THEN("The override wins") { REQUIRE(in.GetInteger("mesh", "nx") == 128); @@ -264,6 +270,7 @@ TEST_CASE("LoadFromRummyStream: ModifyFromCmdline overrides Rummy params", "[Rum TEST_CASE("LoadFromRummyStream: comma-separated vector without brackets", "[Rummy]") { GIVEN("A Rummy stream using bare comma-separated syntax") { ParameterInput in; + in.SetFormat(parthenon::InputFormat::Rummy); std::istringstream ss( "\n" "vals = 1.0, 2.0, 3.0\n" @@ -290,6 +297,7 @@ TEST_CASE("LoadFromRummyStream: comma-separated vector without brackets", "[Rumm TEST_CASE("LoadFromRummyStream: slice assignment syntax", "[Rummy]") { GIVEN("A Rummy stream using slice assignment v[:N] = [...]") { ParameterInput in; + in.SetFormat(parthenon::InputFormat::Rummy); std::istringstream ss( "\n" "v[:3] = [100, 200, 300]\n"); @@ -308,6 +316,7 @@ TEST_CASE("LoadFromRummyStream: slice assignment syntax", "[Rummy]") { TEST_CASE("LoadFromRummyStream: cross-block references are evaluated", "[Rummy]") { GIVEN("A Rummy stream where one block references another block's variable") { ParameterInput in; + in.SetFormat(parthenon::InputFormat::Rummy); std::istringstream ss( "\n" "gamma = 1.4\n" @@ -326,6 +335,7 @@ TEST_CASE("LoadFromRummyStream: cross-block references are evaluated", "[Rummy]" TEST_CASE("LoadFromRummyStream: global variables accessible from blocks", "[Rummy]") { GIVEN("A Rummy stream with a global variable used inside a block") { ParameterInput in; + in.SetFormat(parthenon::InputFormat::Rummy); std::istringstream ss( "Lx = 10.0\n" "\n" @@ -346,6 +356,7 @@ TEST_CASE("LoadFromRummyStream: global variables accessible from blocks", "[Rumm TEST_CASE("LoadFromRummyStream: print statement outside a block", "[Rummy]") { GIVEN("A Rummy stream with a print statement before any block") { ParameterInput in; + in.SetFormat(parthenon::InputFormat::Rummy); // print is a Rummy/pips statement; it produces output but no card. // Verify it doesn't crash and doesn't appear as a parameter. std::istringstream ss( @@ -373,6 +384,7 @@ TEST_CASE("LoadFromRummyStream: print statement outside a block", "[Rummy]") { TEST_CASE("LoadFromRummyStream: vector slice with element-wise math", "[Rummy]") { GIVEN("A Rummy stream that defines a 3-element vector, then cubes a 2-element sub-slice") { ParameterInput in; + in.SetFormat(parthenon::InputFormat::Rummy); // base[:3] defines [2.0, 3.0, 4.0]. // cubed[:2] = base[:2] ** 3 takes only the first two elements and cubes them. std::istringstream ss( @@ -400,6 +412,7 @@ TEST_CASE("LoadFromRummyStream: vector slice with element-wise math", "[Rummy]") TEST_CASE("LoadFromRummyStream: second stream overwrites existing parameters", "[Rummy]") { GIVEN("A first Rummy stream establishing initial values") { ParameterInput in; + in.SetFormat(parthenon::InputFormat::Rummy); std::istringstream ss1( "\n" "nx = 64\n" From 4d3c9f8f15753670a550fda35ef429cc99e1329b Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 3 May 2026 18:14:38 -0600 Subject: [PATCH 11/46] Rummy deck is a unique_ptr now --- src/parameter_input.cpp | 16 ++++++++-------- src/parameter_input.hpp | 6 ++++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/parameter_input.cpp b/src/parameter_input.cpp index e1ac2e5e4691c..725868ecc836a 100644 --- a/src/parameter_input.cpp +++ b/src/parameter_input.cpp @@ -78,9 +78,9 @@ namespace parthenon { //---------------------------------------------------------------------------------------- // ParameterInput constructor -ParameterInput::ParameterInput() : last_filename_{} {} +ParameterInput::ParameterInput() : last_filename_{}, deck_(std::make_unique()) {} -ParameterInput::ParameterInput(std::string input_filename) : last_filename_{} { +ParameterInput::ParameterInput(std::string input_filename) : last_filename_{}, deck_(std::make_unique()) { ReadFile(input_filename); } @@ -351,18 +351,18 @@ void ParameterInput::LoadFromRummyStream(std::istream &is) { PARTHENON_REQUIRE_THROWS(!parsing_finalized_, "Can't add new parameters after parsing is resolved."); - deck.Build(is); + deck_->Build(is); static const std::regex kVectorCardPattern(R"(^(.+)\[(\d+)\]$)"); - for (const auto &suit_name : deck.GetSuitsInOrder()) { + for (const auto &suit_name : deck_->GetSuitsInOrder()) { const std::string &block_name = suit_name; - const auto &suit_cards = deck.GetCardsInOrder(suit_name); + const auto &suit_cards = deck_->GetCardsInOrder(suit_name); for (const auto &card_name : suit_cards) { // match for vector - if (deck.IsCardVector(suit_name, card_name)) { + if (deck_->IsCardVector(suit_name, card_name)) { std::vector comments; - auto elements = deck.GetVector(suit_name, card_name, comments); + auto elements = deck_->GetVector(suit_name, card_name, comments); std::string joined; std::string joined_comments; for (std::size_t i = 0; i < elements.size(); ++i) { @@ -380,7 +380,7 @@ void ParameterInput::LoadFromRummyStream(std::istream &is) { AddParsedParameter(block_name, card_name, UnresolvedString(joined), joined_comments); } else { - auto &card = deck.GetCard(suit_name, card_name); + auto &card = deck_->GetCard(suit_name, card_name); AddParsedParameter(block_name, card_name, ConvertRummyCard(card), card.GetComment()); } } diff --git a/src/parameter_input.hpp b/src/parameter_input.hpp index 47877e1a98414..f732aeab43584 100644 --- a/src/parameter_input.hpp +++ b/src/parameter_input.hpp @@ -46,9 +46,11 @@ #include "utils/sort.hpp" #include "utils/string_utils.hpp" #include "utils/type_list.hpp" -#include "rummy/deck.hpp" #include "utils/utils.hpp" +// Forward-declare Rummy::Deck +namespace Rummy { class Deck; } + namespace parthenon { enum class InputFormat { Native, Rummy, Unknown }; @@ -445,7 +447,7 @@ class ParameterInput { private: InputFormat format = InputFormat::Unknown; - Rummy::Deck deck; + std::unique_ptr deck_; // === PARAMETER STORAGE (vector-of-vectors, preserves insertion order) === std::vector param_storage_; // Ordered storage (for iteration) std::unordered_map From 3ac8149304c291d441e953004e5344887d722c20 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 4 May 2026 07:20:14 -0600 Subject: [PATCH 12/46] Update Rummy --- external/rummy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/rummy b/external/rummy index 0725950493988..7ab22364db991 160000 --- a/external/rummy +++ b/external/rummy @@ -1 +1 @@ -Subproject commit 07259504939887ab310df80fec296ab85c0ed1b9 +Subproject commit 7ab22364db9917e067cd82b1deedc0b3c4cd4d40 From c34a1a395a03fac64efcd9cc9a9361731e476ddf Mon Sep 17 00:00:00 2001 From: Adam Date: Tue, 5 May 2026 06:40:39 -0600 Subject: [PATCH 13/46] update rummy --- external/rummy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/rummy b/external/rummy index 7ab22364db991..c61fe5d6ef812 160000 --- a/external/rummy +++ b/external/rummy @@ -1 +1 @@ -Subproject commit 7ab22364db9917e067cd82b1deedc0b3c4cd4d40 +Subproject commit c61fe5d6ef812d8f0a3201e3cf680e04ed9f1c12 From 1d54e0214b6cfa2eaaa20612426a896b2b9cb7a2 Mon Sep 17 00:00:00 2001 From: Adam Date: Tue, 5 May 2026 08:57:35 -0600 Subject: [PATCH 14/46] Expand command line args and get restarts working with rummy --- src/parameter_input.cpp | 198 +++++++++++++++++++++++++++++++------- src/parameter_input.hpp | 7 +- src/parthenon_manager.cpp | 4 +- 3 files changed, 169 insertions(+), 40 deletions(-) diff --git a/src/parameter_input.cpp b/src/parameter_input.cpp index 725868ecc836a..508b63065b2f5 100644 --- a/src/parameter_input.cpp +++ b/src/parameter_input.cpp @@ -81,12 +81,16 @@ namespace parthenon { ParameterInput::ParameterInput() : last_filename_{}, deck_(std::make_unique()) {} ParameterInput::ParameterInput(std::string input_filename) : last_filename_{}, deck_(std::make_unique()) { - ReadFile(input_filename); + ReadFile(input_filename, false); } -void ParameterInput::ReadFile(const std::string &input_filename) { +void ParameterInput::ReadFile(const std::string &input_filename, const bool is_restart) { if (IsRummyFormat(input_filename)) { + if (is_restart && !deck_initialized_) { + SyncDeckFromStorage(); + } LoadFromRummyFile(input_filename); + deck_initialized_ = true; format = InputFormat::Rummy; } else { IOWrapper infile; @@ -248,21 +252,25 @@ void ParameterInput::LoadFromFile(IOWrapper &input) { } //---------------------------------------------------------------------------------------- -//! \fn bool ParameterInput::IsRummyFormat(const std::string &filename) -// \brief Detect whether a file uses Rummy input format by scanning for markers: +//! \fn bool ParameterInput::IsRummyFormat(std::istream &is) +// \brief Detect whether a stream uses Rummy input format by scanning for markers: // - First line is "# use rummy" (case-insensitive) // - Non-comment, non-blank content before the first line // - Relative suit paths starting with <.. // - Rummy-specific value syntax: ** power operator, quoted strings, // bracket syntax [ ] (vectors/slices), or slice colon inside brackets -bool ParameterInput::IsRummyFormat(const std::string &filename) { - std::ifstream file(filename); - if (!file.is_open()) return false; +bool ParameterInput::IsRummyFormat(std::istream &is) { + const auto start_pos = is.tellg(); + auto restore_and_return = [&](bool result) { + is.clear(); + is.seekg(start_pos); + return result; + }; bool first_line = true; bool found_block = false; std::string line; - while (std::getline(file, line)) { + while (std::getline(is, line)) { line.erase(std::remove_if(line.begin(), line.end(), [](char c) { return std::isspace(c) && c != ' '; }), line.end()); @@ -279,7 +287,7 @@ bool ParameterInput::IsRummyFormat(const std::string &filename) { if (text_start != std::string::npos) { std::string token = after_hash.substr(text_start); std::transform(token.begin(), token.end(), token.begin(), ::tolower); - if (token.compare(0, 9, "use rummy") == 0) return true; + if (token.compare(0, 9, "use rummy") == 0) return restore_and_return(true); } continue; } @@ -289,29 +297,38 @@ bool ParameterInput::IsRummyFormat(const std::string &filename) { if (line.compare(first_char, 1, "<") == 0) { if (line.size() > first_char + 2 && line.compare(first_char + 1, 2, "..") == 0) - return true; + return restore_and_return(true); found_block = true; continue; } // Non-comment, non-blank content before the first block = Rummy global variable - if (!found_block) return true; + if (!found_block) return restore_and_return(true); // Rummy-specific syntax in the value part auto eq_pos = line.find('='); if (eq_pos != std::string::npos) { + std::string name_part = line.substr(first_char, eq_pos - first_char); + if (name_part.find_first_of(".[") != std::string::npos) return restore_and_return(true); + std::string value_part = line.substr(eq_pos + 1); - if (value_part.find("**") != std::string::npos) return true; // power operator - if (value_part.find('"') != std::string::npos) return true; // quoted string - if (value_part.find('[') != std::string::npos) return true; // vector/slice syntax + if (value_part.find_first_of("*\"[+-/%^|") != std::string::npos) return restore_and_return(true); } // Slice syntax on the LHS: name[:2] or name[0:2] std::string lhs = line.substr(first_char, eq_pos == std::string::npos ? std::string::npos : eq_pos - first_char); - if (lhs.find('[') != std::string::npos) return true; + if (lhs.find('[') != std::string::npos) return restore_and_return(true); } - return false; + return restore_and_return(false); +} + +//! \fn bool ParameterInput::IsRummyFormat(const std::string &filename) +// \brief Detect whether a file uses Rummy input format. Delegates to the stream overload. +bool ParameterInput::IsRummyFormat(const std::string &filename) { + std::ifstream file(filename); + if (!file.is_open()) return false; + return IsRummyFormat(file); } //---------------------------------------------------------------------------------------- @@ -319,9 +336,20 @@ bool ParameterInput::IsRummyFormat(const std::string &filename) { namespace { -//! \fn ParameterInput::ParamValue ConvertRummyCard(const Rummy::Card &card) +//! \fn std::string SanitizeString(const std::string &input) +// \brief Strip leading/trailing whitespace and inline comments. +std::string SanitizeString(const std::string &input) { + std::string output = input.substr(0, input.find('#')); // remove trailing comment + output.erase(output.begin(), + std::find_if(output.begin(), output.end(), [](char c) { return !std::isspace(c); })); + output.erase(std::find_if(output.rbegin(), output.rend(), [](char c) { return !std::isspace(c); }) + .base(), + output.end()); + return output; +} +//! \fn ParameterInput::ParamValue RummyCardToParamValue(const Rummy::Card &card) // \brief Convert a Rummy Card to a ParameterInput::ParamValue for storage in ParameterInput. -ParameterInput::ParamValue ConvertRummyCard(const Rummy::Card &card) { +ParameterInput::ParamValue RummyCardToParamValue(const Rummy::Card &card) { if (card.isBool()) { return card.Get(); } else if (card.isString()) { @@ -333,13 +361,33 @@ ParameterInput::ParamValue ConvertRummyCard(const Rummy::Card &card) { } } -//! \fn std::string RummyCardToString(const Rummy::Card &card) -// \brief Convert a Rummy Card to its string representation. -std::string RummyCardToString(const Rummy::Card &card) { - if (card.isBool()) { - return card.Get() ? "true" : "false"; - } - return card.GetString(std::numeric_limits::max_digits10); +//! \fn Rummy::Card ParamValueToRummyCard(suit, name, v) +// \brief Convert a scalar ParamValue to a Rummy::Card. +Rummy::Card ParamValueToRummyCard(const std::string &suit, const std::string &name, + const ParameterInput::ParamValue &v) { + if (std::holds_alternative(v)) + return Rummy::Card(suit, name, std::get(v), ""); + if (std::holds_alternative(v)) + return Rummy::Card(suit, name, static_cast(std::get(v)), ""); + if (std::holds_alternative(v)) + return Rummy::Card(suit, name, static_cast(std::get(v)), ""); + if (std::holds_alternative(v)) + return Rummy::Card(suit, name, std::get(v), ""); + // UnresolvedString + const std::string &raw = std::get(v).value; + std::string trimmed = SanitizeString(raw); + + std::string lower = trimmed; + std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower); + + if (lower == "true") return Rummy::Card(suit, name, true, ""); + if (lower == "false") return Rummy::Card(suit, name, false, ""); + try { + std::size_t pos; + double d = std::stod(trimmed, &pos); + return Rummy::Card(suit, name, d, ""); + } catch (...) {} + return Rummy::Card(suit, name, trimmed, ""); } } // anonymous namespace @@ -377,17 +425,92 @@ void ParameterInput::LoadFromRummyStream(std::istream &is) { } joined += elements[i]; } - AddParsedParameter(block_name, card_name, UnresolvedString(joined), - joined_comments); + // Rummy stores comments without '#' + std::string comment; + if (!joined_comments.empty()) comment = "# " + joined_comments; + AddParsedParameter(block_name, card_name, UnresolvedString(joined), comment); } else { auto &card = deck_->GetCard(suit_name, card_name); - AddParsedParameter(block_name, card_name, ConvertRummyCard(card), card.GetComment()); + std::string comment; + if (!card.GetComment().empty()) comment = "# " + card.GetComment(); + AddParsedParameter(block_name, card_name, RummyCardToParamValue(card), comment); } } } } //---------------------------------------------------------------------------------------- +//! \fn void ParameterInput::SyncDeckFromStorage() +// \brief Seed the Rummy Deck from the current param_storage_ contents. +void ParameterInput::SyncDeckFromStorage() { + std::map> new_cards; + std::vector new_suits; + std::map> new_card_map; + + // Register a single card into the three structures, adding the suit on first use. + auto register_card = [&](const std::string &suit, const std::string &card_name, + Rummy::Card card) { + if (new_cards.find(suit) == new_cards.end()) { + new_suits.push_back(suit); + new_card_map[suit] = {}; + } + new_card_map[suit].push_back(card_name); + new_cards[suit][card_name] = std::move(card); + }; + + for (const auto &block : param_storage_) { + // Collapse the block name into a Rummy suit: non-empty '/' segments joined by '/'. + // A block that is only "/" (global scope) maps to suit "/". + std::string suit = "/"; + { + std::string assembled; + std::istringstream bss(block.name); + std::string part; + while (std::getline(bss, part, '/')) { + if (!part.empty()) { + if (!assembled.empty()) assembled += '/'; + assembled += part; + } + } + if (!assembled.empty()) suit = assembled; + } + + for (const auto ¶m : block.params) { + // Vector variants expand to one card per element: name[0], name[1], ... + if (std::holds_alternative>(param.value)) { + const auto &vec = std::get>(param.value); + for (size_t i = 0; i < vec.size(); ++i) { + std::string cn = param.name + "[" + std::to_string(i) + "]"; + register_card(suit, cn, Rummy::Card(suit, cn, static_cast(vec[i]), "")); + } + } else if (std::holds_alternative>(param.value)) { + const auto &vec = std::get>(param.value); + for (size_t i = 0; i < vec.size(); ++i) { + std::string cn = param.name + "[" + std::to_string(i) + "]"; + register_card(suit, cn, Rummy::Card(suit, cn, static_cast(vec[i]), "")); + } + } else if (std::holds_alternative>(param.value)) { + const auto &vec = std::get>(param.value); + for (size_t i = 0; i < vec.size(); ++i) { + std::string cn = param.name + "[" + std::to_string(i) + "]"; + register_card(suit, cn, Rummy::Card(suit, cn, static_cast(vec[i]), "")); + } + } else if (std::holds_alternative>(param.value)) { + const auto &vec = std::get>(param.value); + for (size_t i = 0; i < vec.size(); ++i) { + std::string cn = param.name + "[" + std::to_string(i) + "]"; + register_card(suit, cn, Rummy::Card(suit, cn, vec[i], "")); + } + } else { + register_card(suit, param.name, + ParamValueToRummyCard(suit, param.name, param.value)); + } + } + } + + deck_->SeedGlobals(new_cards, new_suits, new_card_map); +} + //! \fn void ParameterInput::LoadFromRummyFile(const std::string &filename) // \brief MPI-safe loader for Rummy-format input files. void ParameterInput::LoadFromRummyFile(const std::string &filename) { @@ -529,15 +652,18 @@ void ParameterInput::ModifyFromCmdline(std::vector mods) { "Can't add new parameters to the linked list after the map is resolved."); if (mods.empty()) return; - PARTHENON_REQUIRE_THROWS( - format != InputFormat::Unknown, - "Can't determine the input format."); - if (format == InputFormat::Rummy) { - std::stringstream ss; - for (const auto &mod : mods) { - ss << mod << "\n"; + std::stringstream ss; + for (const auto &mod : mods) { + ss << mod << "\n"; + } + + if (format == InputFormat::Rummy || IsRummyFormat(ss)) { + if (!deck_initialized_) { + SyncDeckFromStorage(); + deck_initialized_ = true; } LoadFromRummyStream(ss); + format = InputFormat::Rummy; return; } @@ -545,7 +671,7 @@ void ParameterInput::ModifyFromCmdline(std::vector mods) { std::string input_text, block, name, value; std::stringstream msg; - for (const auto &input_text : mods) { + while(std::getline(ss, input_text)) { std::size_t equal_posn = input_text.find_first_of("="); // first "=" character std::size_t slash_posn = input_text.rfind("/", equal_posn); // last "/" before "=" diff --git a/src/parameter_input.hpp b/src/parameter_input.hpp index f732aeab43584..17d5c809c3c4f 100644 --- a/src/parameter_input.hpp +++ b/src/parameter_input.hpp @@ -220,13 +220,15 @@ class ParameterInput { ParameterInput(); explicit ParameterInput(std::string input_filename); ~ParameterInput(); - void ReadFile(const std::string &input_filename); + void ReadFile(const std::string &input_filename, const bool is_restart); // === PARSING INTERFACE === void LoadFromStream(std::istream &is); void LoadFromFile(IOWrapper &input); void LoadFromRummyFile(const std::string &filename); void LoadFromRummyStream(std::istream &is); + void SyncDeckFromStorage(); + static bool IsRummyFormat(std::istream &is); static bool IsRummyFormat(const std::string &filename); void ModifyFromCmdline(std::vector mods); @@ -446,7 +448,8 @@ class ParameterInput { private: - InputFormat format = InputFormat::Unknown; + InputFormat format = InputFormat::Native; + bool deck_initialized_ = false; std::unique_ptr deck_; // === PARAMETER STORAGE (vector-of-vectors, preserves insertion order) === std::vector param_storage_; // Ordered storage (for iteration) diff --git a/src/parthenon_manager.cpp b/src/parthenon_manager.cpp index c381588bb14c5..21d1bd4962967 100644 --- a/src/parthenon_manager.cpp +++ b/src/parthenon_manager.cpp @@ -124,13 +124,13 @@ ParthenonStatus ParthenonManager::ParthenonInitEnv(int argc, char *argv[]) { // Modify info read from restart file if (arg.is_restart) { for(const auto &input_filename : arg.input_filenames) { - pinput->ReadFile(input_filename); + pinput->ReadFile(input_filename, arg.is_restart); } // Populate new object for fresh simulation } else { pinput = std::make_unique(); for (const auto &input_filename : arg.input_filenames) { - pinput->ReadFile(input_filename); + pinput->ReadFile(input_filename, arg.is_restart); } } } From c9b93810dd2ea1bab734d4df665327b4c04c0b6d Mon Sep 17 00:00:00 2001 From: Adam Date: Tue, 5 May 2026 09:14:59 -0600 Subject: [PATCH 15/46] Add a rummy driven sparse_advection regression test --- tst/regression/CMakeLists.txt | 7 + .../sparse_advection_rummy/__init__.py | 0 .../parthinput.sparse_advection_rummy | 83 +++++++++++ .../sparse_advection_rummy.py | 132 ++++++++++++++++++ 4 files changed, 222 insertions(+) create mode 100644 tst/regression/test_suites/sparse_advection_rummy/__init__.py create mode 100644 tst/regression/test_suites/sparse_advection_rummy/parthinput.sparse_advection_rummy create mode 100644 tst/regression/test_suites/sparse_advection_rummy/sparse_advection_rummy.py diff --git a/tst/regression/CMakeLists.txt b/tst/regression/CMakeLists.txt index 273ae4c942899..00f740d489e08 100644 --- a/tst/regression/CMakeLists.txt +++ b/tst/regression/CMakeLists.txt @@ -151,6 +151,13 @@ if (ENABLE_HDF5) --num_steps 3") list(APPEND EXTRA_TEST_LABELS "") + list(APPEND TEST_DIRS sparse_advection_rummy) + list(APPEND TEST_PROCS ${NUM_MPI_PROC_TESTING}) + list(APPEND TEST_ARGS "--driver ${PROJECT_BINARY_DIR}/example/sparse_advection/sparse_advection-example \ + --driver_input ${CMAKE_CURRENT_SOURCE_DIR}/test_suites/sparse_advection/parthinput.sparse_advection_rummy \ + --num_steps 3") + list(APPEND EXTRA_TEST_LABELS "") + list(APPEND TEST_DIRS particle_tracers) list(APPEND TEST_PROCS ${NUM_MPI_PROC_TESTING}) list(APPEND TEST_ARGS "--driver ${PROJECT_BINARY_DIR}/example/particle_tracers/particle-tracers \ diff --git a/tst/regression/test_suites/sparse_advection_rummy/__init__.py b/tst/regression/test_suites/sparse_advection_rummy/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tst/regression/test_suites/sparse_advection_rummy/parthinput.sparse_advection_rummy b/tst/regression/test_suites/sparse_advection_rummy/parthinput.sparse_advection_rummy new file mode 100644 index 0000000000000..710898770ceda --- /dev/null +++ b/tst/regression/test_suites/sparse_advection_rummy/parthinput.sparse_advection_rummy @@ -0,0 +1,83 @@ +# ======================================================================================== +# Athena++ astrophysical MHD code +# Copyright(C) 2014 James M. Stone and other code contributors +# Licensed under the 3-clause BSD License, see LICENSE file for details +# ======================================================================================== +# (C) (or copyright) 2021-2026. Triad National Security, LLC. All rights reserved. +# +# This program was produced under U.S. Government contract 89233218CNA000001 for Los +# Alamos National Laboratory (LANL), which is operated by Triad National Security, LLC +# for the U.S. Department of Energy/National Nuclear Security Administration. All rights +# in the program are reserved by Triad National Security, LLC, and the U.S. Department +# of Energy/National Nuclear Security Administration. The Government is granted for +# itself and others acting on its behalf a nonexclusive, paid-up, irrevocable worldwide +# license in this material to reproduce, prepare derivative works, distribute copies to +# the public, perform publicly and display publicly, and to permit others to do so. +# ======================================================================================== + +# This is an example of a Rummy input file + + +# Global variables + +NB = 1 << 3 # Rummy supports bit shifts +N = NB << 3 + +L = 1.0, 1.0, 1.0 + + + +problem_id = "sparse" + + +enable_sparse = true +alloc_threshold = 1e-6 +dealloc_threshold = 0.1 * alloc_threshold # 10% of alloc threshold +dealloc_count = 5 + + +refinement = "adaptive" +numlevel = 3 + +nx1 = N +x1min = -L[0] +x1max = L[0] +ix1_bc = "periodic" +ox1_bc = ix1_bc + +nx2 = N +x2min = -L[1] +x2max = L[1] +ix2_bc = "reflecting" +ox2_bc = "outflow" + +nx3 = 1 +x3min = -L[2] +x3max = L[2] +ix3_bc = "periodic" +ox3_bc = ix3_bc + + +nx1 = NB +nx2 = NB +nx3 = 1 + + +recv_bdry_buf_timeout_sec = 10 +nlim = -1 +tlim = 1.0 +integrator = "rk2" +ncycle_out_mesh = -10000 +comm_buffer_reset_cadence = 10 + + +cfl = 0.45 +speed = 1.5 + +refine_tol = 0.3 # control the package specific refinement tagging function +derefine_tol = 0.1 * derefine_tol + + +file_type = "hdf5" +dt = 0.5 +variables = "sparse" diff --git a/tst/regression/test_suites/sparse_advection_rummy/sparse_advection_rummy.py b/tst/regression/test_suites/sparse_advection_rummy/sparse_advection_rummy.py new file mode 100644 index 0000000000000..98bbb1748cf43 --- /dev/null +++ b/tst/regression/test_suites/sparse_advection_rummy/sparse_advection_rummy.py @@ -0,0 +1,132 @@ +# ======================================================================================== +# Parthenon performance portable AMR framework +# Copyright(C) 2021 The Parthenon collaboration +# Licensed under the 3-clause BSD License, see LICENSE file for details +# ======================================================================================== +# (C) (or copyright) 2021. Triad National Security, LLC. All rights reserved. +# +# This program was produced under U.S. Government contract 89233218CNA000001 for Los +# Alamos National Laboratory (LANL), which is operated by Triad National Security, LLC +# for the U.S. Department of Energy/National Nuclear Security Administration. All rights +# in the program are reserved by Triad National Security, LLC, and the U.S. Department +# of Energy/National Nuclear Security Administration. The Government is granted for +# itself and others acting on its behalf a nonexclusive, paid-up, irrevocable worldwide +# license in this material to reproduce, prepare derivative works, distribute copies to +# the public, perform publicly and display publicly, and to permit others to do so. +# ======================================================================================== + +# Modules +import sys +import utils.test_case + +# To prevent littering up imported folders with .pyc files or __pycache_ folder +sys.dont_write_bytecode = True + + +class TestCase(utils.test_case.TestCaseAbs): + def Prepare(self, parameters, step): + + parameters.coverage_status = "both" + + if parameters.sparse_disabled: + parameters.driver_cmd_line_args = [ + "parthenon.sparse.enable_sparse=false", + ] + + # Run a test with two trees + if step == 2: + parameters.driver_cmd_line_args = [ + "parthenon.mesh.nx2=32", + "parthenon.job.problem_id=\"sparse_twotree\"", + ] + + # Run a test with two trees and a statically refined region + if step == 3: + parameters.driver_cmd_line_args = [ + "parthenon.mesh.nx2=32", + "parthenon.time.nlim=50", + "parthenon.job.problem_id=\"sparse_twotree_static\"", + "parthenon.mesh.refinement=static", + "parthenon.static_refinement0.x1min=-0.75", + "parthenon.static_refinement0.x1max=-0.5", + "parthenon.static_refinement0.x2min=-0.75", + "parthenon.static_refinement0.x2max=-0.5", + "parthenon.static_refinement0.level=3", + ] + + return parameters + + def Analyse(self, parameters): + + sys.path.insert( + 1, + parameters.parthenon_path + + "/scripts/python/packages/parthenon_tools/parthenon_tools", + ) + + try: + from phdf_diff import compare + except ModuleNotFoundError: + print("Couldn't find module to compare Parthenon hdf5 files.") + return False + + # compare against fake sparse version, needs to match up to tolerance used for sparse allocation + delta = compare( + [ + "sparse.out0.final.phdf", + parameters.parthenon_path + + "/tst/regression/gold_standard/sparse_fake.out0.final.phdf", + ], + one=True, + tol=2e-6, + # don't check metadata, because SparseInfo will differ + check_metadata=False, + ) + + if delta != 0: + return False + + if not parameters.sparse_disabled: + # compare against true sparse, needs to match to machine precision + delta = compare( + [ + "sparse.out0.final.phdf", + parameters.parthenon_path + + "/tst/regression/gold_standard/sparse_true.out0.final.phdf", + ], + one=True, + tol=1e-12, + check_metadata=False, + ) + if delta != 0: + print("Sparse advection failed for standard AMR grid setup.") + return False + + delta = compare( + [ + "sparse_twotree.out0.final.phdf", + parameters.parthenon_path + + "/tst/regression/gold_standard/sparse_twotree.out0.final.phdf", + ], + one=True, + tol=1e-12, + check_metadata=False, + ) + if delta != 0: + print("Sparse advection failed for two-tree AMR grid setup.") + return False + + delta = compare( + [ + "sparse_twotree_static.out0.final.phdf", + parameters.parthenon_path + + "/tst/regression/gold_standard/sparse_twotree_static.out0.final.phdf", + ], + one=True, + tol=1e-12, + check_metadata=False, + ) + if delta != 0: + print("Sparse advection failed for two-tree SMR grid setup.") + + return delta == 0 From a8b9744a685a067150ef9a395a98e8999abc5092 Mon Sep 17 00:00:00 2001 From: Adam Date: Tue, 5 May 2026 10:24:07 -0600 Subject: [PATCH 16/46] Add docs for rummy --- doc/sphinx/src/chapters/getting_started.rst | 1 + doc/sphinx/src/rummy_input.rst | 444 ++++++++++++++++++++ 2 files changed, 445 insertions(+) create mode 100644 doc/sphinx/src/rummy_input.rst diff --git a/doc/sphinx/src/chapters/getting_started.rst b/doc/sphinx/src/chapters/getting_started.rst index 2017457cc9941..bc1a44d780b75 100644 --- a/doc/sphinx/src/chapters/getting_started.rst +++ b/doc/sphinx/src/chapters/getting_started.rst @@ -11,4 +11,5 @@ so you can run your first example quickly. ../README ../building ../inputs + ../rummy_input ../outputs diff --git a/doc/sphinx/src/rummy_input.rst b/doc/sphinx/src/rummy_input.rst new file mode 100644 index 0000000000000..4aa506c25715b --- /dev/null +++ b/doc/sphinx/src/rummy_input.rst @@ -0,0 +1,444 @@ +.. _rummy_input: + +Rummy Input Files +================= + +Parthenon supports an extended input file format called **Rummy**, in addition +to the native ``param = value`` format. Rummy retains full backwards +compatibility with native input files while adding expression evaluation, +global variables, vector parameters, relative block paths, file inclusion, +and more. + +Rummy is auto-detected — no special build flag is required. Whether a +particular input file is parsed by Rummy or the native parser is determined +automatically at runtime (see :ref:`rummy_detection`). + +.. contents:: + :local: + :depth: 2 + + +Basic Syntax +------------ + +Rummy input files share the same block/parameter structure as native files: + +.. code-block:: text + + + param = value # optional inline comment + other = 1.23 + +The key difference is that Rummy compiles the entire file from the top down, +so any card defined earlier can be referenced by name in a later expression. + + +Global Variables +---------------- + +Cards declared **before** the first ```` header are *global variables*. +They live in the unnamed ``/`` suit and can be referenced by name anywhere in +the file without a block qualifier: + +.. code-block:: text + + L = 1.0 + rho = 2.5 + + + Lx = L # reference the global variable L + Ly = L + +Global variables are the simplest Rummy feature. Their presence (content +before the first ``<...>`` line) is one of the markers that causes Parthenon +to choose the Rummy parser automatically. + + +Expression Evaluation +--------------------- + +Parameter values can be arbitrary arithmetic expressions. + +**Arithmetic** + ++------------+-------------------------------+ +| Syntax | Meaning | ++============+===============================+ +| ``+ -`` | Addition / subtraction | ++------------+-------------------------------+ +| ``* /`` | Multiplication / division | ++------------+-------------------------------+ +| ``//`` | Integer (floor) division | ++------------+-------------------------------+ +| ``**`` | Power (e.g. ``2**10``) | ++------------+-------------------------------+ +| ``%`` | Modulo | ++------------+-------------------------------+ +| ``pi`` | Named constant Ï€ | ++------------+-------------------------------+ + +**Boolean** (operate on ``true``/``false`` values) + ++----------+----------------------+ +| Syntax | Meaning | ++==========+======================+ +| ``and`` | Logical AND | ++----------+----------------------+ +| ``or`` | Logical OR | ++----------+----------------------+ +| ``xor`` | Logical XOR | ++----------+----------------------+ +| ``not`` | Logical NOT (unary) | ++----------+----------------------+ + +**Bitwise** (operate on integers; also work on booleans) + ++--------+---------------------+ +| Syntax | Meaning | ++========+=====================+ +| ``&`` | Bitwise AND | ++--------+---------------------+ +| ``|`` | Bitwise OR | ++--------+---------------------+ +| ``^`` | Bitwise XOR | ++--------+---------------------+ +| ``~`` | Bitwise NOT (unary) | ++--------+---------------------+ +| ``<<`` | Left shift | ++--------+---------------------+ +| ``>>`` | Right shift | ++--------+---------------------+ + +**Comparison**: ``==``, ``!=``, ``<``, ``<=``, ``>``, ``>=`` + +Examples: + +.. code-block:: text + + dt = 0.45 * dx + gamma = 5.0/3.0 + cv = 1.0/(gamma - 1.0) + vol = L**3 + half_nx = nx // 2 + use_mhd = hydro and conduction + +Values from **any previously-defined card** (including cards in other blocks) +can be referenced by their fully-qualified dotted name: + +.. code-block:: text + + + gamma = 5.0/3.0 + + + cv = 1.0/(eos.gamma - 1.0) + + +Math Functions +-------------- + +Almost all ```` functions are available as built-in keywords: + +**Trigonometric** — ``sin``, ``cos``, ``tan``, ``asin``, ``acos``, ``atan``, +``atan2(y, x)`` + +**Exponential / logarithm** — ``exp``, ``log`` (natural), ``log10`` + +**Power / rounding** — ``sqrt``, ``ceil``, ``floor``, ``abs``, ``sign`` + +**Extrema** — ``min(a, b)``, ``max(a, b)`` + +.. code-block:: text + + theta = pi / 4.0 + vx = v * cos(theta) + vy = v * sin(theta) + r = sqrt(vx**2 + vy**2) + lo = min(r, 1.0) + + +Ternary Operator +---------------- + +The C-style ternary ``condition ? value_if_true : value_if_false`` is +supported: + +.. code-block:: text + + nx = 64 + ny = (nx > 32) ? nx // 2 : nx # ny = 32 + label = (debug) ? "debug" : "production" + + +String Parameters +----------------- + +String values must be quoted: + +.. code-block:: text + + + problem_id = "advection" + +String concatenation uses ``+``: + +.. code-block:: text + + prefix = "my_" + + label = prefix + "run" + + +Boolean Parameters +------------------ + +Boolean values are written as ``true`` or ``false`` (case-insensitive): + +.. code-block:: text + + + hydro = true + conduction = false + do_work = hydro or conduction + both = hydro and conduction + neither = not hydro and not conduction + + +Vector Parameters +----------------- + +Vectors are comma-separated lists. Both bare and bracketed syntax work: + +.. code-block:: text + + L = 1.0, 1.0, 0.5 # bare comma list + n = [10, 10, 1] # bracket syntax + +Individual elements are accessed with zero-based indexing: + +.. code-block:: text + + + nx1 = n[0] + nx2 = n[1] + +**Slice assignments** copy a range of elements from a vector into another: + +.. code-block:: text + + xmin = -L[0]/2., -L[1]/2., -L[2]/2. + xmax[:] = 0.5 * L[:3] # broadcast scalar * slice + +The ``[:]`` slice on the left-hand side means "all elements"; ``[:3]`` means +elements 0, 1, 2. + + +Relative Block Paths +-------------------- + +A block header starting with ``<../`` declares a child of the *current* block: + +.. code-block:: text + + + name = "hydrogen" + + <../eos> # expands to + gamma = 5.0/3.0 + + <../conductivity> # expands to + kappa = 0.1 / gas.eos.gamma + +Relative paths allow logically related sub-blocks to stay near each other in +the file without repeating long prefixes. + + +Including Other Files +--------------------- + +The ``include`` statement inserts another file into the current compilation +at that point. All variables defined before the ``include`` are visible +inside the included file, and all variables defined inside are visible after +it returns: + +.. code-block:: text + + # main.par + # use rummy + + L = 1.0 + nx = 64 + + include "mesh.par" # relative to the directory of main.par + include "/abs/path/eos.par" # absolute path also works + +Circular includes are detected and cause a fatal error. Paths are resolved +relative to the directory of the file containing the ``include`` statement. + + +Multiple Input Files +-------------------- + +Multiple ``-i`` arguments can be passed on the command line. All files are +compiled in the **same Rummy compilation space**, so variables defined in an +earlier file are available in later ones: + +.. code-block:: bash + + ./my-app -i base.par -i overrides.par parthenon.time.nlim=100 + +.. code-block:: text + + # base.par + # use rummy + nx = 64 + + nx1 = nx + +.. code-block:: text + + # overrides.par — sees nx from base.par + nx = 128 # redefines nx for the higher-resolution run + +Files are read in the order they appear on the command line. + + +Debugging Utilities +------------------- + +Three special statements print the current state of the compiler to +standard output and are useful for debugging input decks: + +``__locals__`` + Print all variables local to the current block (suit). + +``__globals__`` + Print all globally defined variables (including cards from all + previously compiled suits). + +``__stack__`` + Print the current expression evaluation stack. + +``__list__`` + The combined output of ``__globals__``, ``__locals__``, and ``__stack__``. + +.. code-block:: text + + + nx = 64 + __globals__ # prints all globals including nx + + +The ``print`` Function +---------------------- + +The built-in variadic ``print`` function writes any previously compiled card's value to +standard output when the deck is loaded. It is useful for sanity-checking +derived quantities: + +.. code-block:: text + + dt = 0.45 * dx + print("dt = ", dt) + +Only cards defined *before* the ``print`` call can be printed. + + +Multiline Expressions +--------------------- + +A trailing ``&`` continues an expression on the next line: + +.. code-block:: text + + long_value = 1.0 & + + 2.0 & + + 3.0 # = 6.0 + + +.. _rummy_detection: + +How Parthenon Detects Rummy Files +---------------------------------- + +Parthenon auto-detects Rummy format without any explicit flag. A file (or +command-line override string) is routed to the Rummy parser if **any** of the +following is true: + +1. The first non-blank line is ``# use rummy`` (case-insensitive) — the + explicit opt-in marker. +2. There is non-comment, non-blank content **before** the first ```` + header (i.e., global variables are present). +3. A block header begins with ``<..`` (relative path syntax). +4. A parameter **name** contains ``.`` or ``[`` (dotted reference or vector + index on the LHS). +5. A parameter **value** contains any of: ``**``, ``"``, ``[``, ``+``, ``-``, + ``/``, ``%``, ``^``, or ``|`` (expression operators or quoted strings). + +To unconditionally use the Rummy parser, place ``# use rummy`` as the very +first line of the file: + +.. code-block:: text + + # use rummy + + nlim = 100 + tlim = 1.0 + +A native input file (no Rummy syntax) continues to be parsed by the native +parser transparently. + + +Restart Files and Rummy +----------------------- + +When restarting from an HDF5 restart file (``-r``), Parthenon loads the +parameter snapshot stored in the restart file using the native parser, then +layers any Rummy input file (``-i``) and command-line overrides on top. + +The Rummy deck is seeded with all parameters from the restart before the Rummy +file is compiled, so expressions in the Rummy file can reference values that +came from the restart. For example: + +.. code-block:: bash + + ./my-app -r run.out1.final.rhdf -i params.par parthenon.time.nlim=10 + +``parthenon.time.nlim=10`` is itself detected as a Rummy override (native +overrides use ``block/param=value`` syntax) and is applied after the restart +parameters are seeded into the deck. + + +Example +------- + +A complete minimal Rummy input file: + +.. code-block:: text + + # use rummy + + # --- Global parameters --- + L = 1.0 + nx = 64 + + include "common_physics.par" + + + nx1 = nx + nx2 = nx + x1min = -L/2. + x1max = L/2. + x2min = -L/2. + x2max = L/2. + + + tlim = 2.0 + nlim = -1 + dt = 0.45 * L / nx # CFL-based initial guess + + <../output> # relative: expands to parthenon/output + file_type = "hdf5" + dt = 0.1 + + __globals__ # print all compiled globals for debugging From 0a5da2bbdc3751229cb5cc90b9b304e9f4522bc8 Mon Sep 17 00:00:00 2001 From: Adam Dempsey Date: Tue, 5 May 2026 12:38:18 -0600 Subject: [PATCH 17/46] Update rummy --- external/rummy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/rummy b/external/rummy index c61fe5d6ef812..0a7d9fd80de7b 160000 --- a/external/rummy +++ b/external/rummy @@ -1 +1 @@ -Subproject commit c61fe5d6ef812d8f0a3201e3cf680e04ed9f1c12 +Subproject commit 0a7d9fd80de7b743390558b666c07ab4a0afb26e From 911529068972817bbe581e8c14158b1393b31720 Mon Sep 17 00:00:00 2001 From: Adam Dempsey Date: Tue, 5 May 2026 14:39:10 -0600 Subject: [PATCH 18/46] Just add_subdirectory rummy --- CMakeLists.txt | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 35edce3a4a015..661ac09aeabdf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -445,16 +445,9 @@ find_package(Rummy QUIET) if (NOT Rummy_FOUND) # If Rummy is not found, instead use the git submodule - if (NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/external/rummy/single_include) - # Unable to find the header files for Rummy or they don't exist - message(STATUS "Downloading Rummy submodule.") - - # Clone the submodule - execute_process(COMMAND git submodule update --init --force -- external/rummy WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) - endif() - + set(RUMMY_ENABLE_COVERAGE OFF CACHE BOOL "Disable Rummy coverage" FORCE) + set(RUMMY_ENABLE_UNIT_TESTS OFF CACHE BOOL "Disable Rummy unit tests" FORCE) add_subdirectory(external/rummy) - list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/external/rummy/contrib") endif() From a526c9fc4acdb61ad8d6a88f004595bb6b83b88f Mon Sep 17 00:00:00 2001 From: Adam Dempsey Date: Tue, 5 May 2026 16:46:35 -0600 Subject: [PATCH 19/46] format --- src/argument_parser.hpp | 2 +- src/parameter_input.cpp | 55 ++++++++++++++++++++++----------------- src/parameter_input.hpp | 5 ++-- src/parthenon_manager.cpp | 6 ++--- 4 files changed, 38 insertions(+), 30 deletions(-) diff --git a/src/argument_parser.hpp b/src/argument_parser.hpp index 5034112bae157..5911279e83016 100644 --- a/src/argument_parser.hpp +++ b/src/argument_parser.hpp @@ -129,7 +129,7 @@ class ArgParse { return ArgStatus::error; } } else { - modifiers.push_back(argv[i]); + modifiers.push_back(argv[i]); } } diff --git a/src/parameter_input.cpp b/src/parameter_input.cpp index 508b63065b2f5..a3cc22d525201 100644 --- a/src/parameter_input.cpp +++ b/src/parameter_input.cpp @@ -78,9 +78,11 @@ namespace parthenon { //---------------------------------------------------------------------------------------- // ParameterInput constructor -ParameterInput::ParameterInput() : last_filename_{}, deck_(std::make_unique()) {} +ParameterInput::ParameterInput() + : last_filename_{}, deck_(std::make_unique()) {} -ParameterInput::ParameterInput(std::string input_filename) : last_filename_{}, deck_(std::make_unique()) { +ParameterInput::ParameterInput(std::string input_filename) + : last_filename_{}, deck_(std::make_unique()) { ReadFile(input_filename, false); } @@ -309,22 +311,25 @@ bool ParameterInput::IsRummyFormat(std::istream &is) { auto eq_pos = line.find('='); if (eq_pos != std::string::npos) { std::string name_part = line.substr(first_char, eq_pos - first_char); - if (name_part.find_first_of(".[") != std::string::npos) return restore_and_return(true); + if (name_part.find_first_of(".[") != std::string::npos) + return restore_and_return(true); std::string value_part = line.substr(eq_pos + 1); - if (value_part.find_first_of("*\"[+-/%^|") != std::string::npos) return restore_and_return(true); + if (value_part.find_first_of("*\"[+-/%^|") != std::string::npos) + return restore_and_return(true); } // Slice syntax on the LHS: name[:2] or name[0:2] - std::string lhs = line.substr(first_char, eq_pos == std::string::npos - ? std::string::npos - : eq_pos - first_char); + std::string lhs = + line.substr(first_char, eq_pos == std::string::npos ? std::string::npos + : eq_pos - first_char); if (lhs.find('[') != std::string::npos) return restore_and_return(true); } return restore_and_return(false); } //! \fn bool ParameterInput::IsRummyFormat(const std::string &filename) -// \brief Detect whether a file uses Rummy input format. Delegates to the stream overload. +// \brief Detect whether a file uses Rummy input format. Delegates to the stream +// overload. bool ParameterInput::IsRummyFormat(const std::string &filename) { std::ifstream file(filename); if (!file.is_open()) return false; @@ -339,16 +344,18 @@ namespace { //! \fn std::string SanitizeString(const std::string &input) // \brief Strip leading/trailing whitespace and inline comments. std::string SanitizeString(const std::string &input) { - std::string output = input.substr(0, input.find('#')); // remove trailing comment - output.erase(output.begin(), - std::find_if(output.begin(), output.end(), [](char c) { return !std::isspace(c); })); - output.erase(std::find_if(output.rbegin(), output.rend(), [](char c) { return !std::isspace(c); }) - .base(), - output.end()); - return output; + std::string output = input.substr(0, input.find('#')); // remove trailing comment + output.erase(output.begin(), std::find_if(output.begin(), output.end(), + [](char c) { return !std::isspace(c); })); + output.erase(std::find_if(output.rbegin(), output.rend(), + [](char c) { return !std::isspace(c); }) + .base(), + output.end()); + return output; } //! \fn ParameterInput::ParamValue RummyCardToParamValue(const Rummy::Card &card) -// \brief Convert a Rummy Card to a ParameterInput::ParamValue for storage in ParameterInput. +// \brief Convert a Rummy Card to a ParameterInput::ParamValue for storage in +// ParameterInput. ParameterInput::ParamValue RummyCardToParamValue(const Rummy::Card &card) { if (card.isBool()) { return card.Get(); @@ -364,7 +371,7 @@ ParameterInput::ParamValue RummyCardToParamValue(const Rummy::Card &card) { //! \fn Rummy::Card ParamValueToRummyCard(suit, name, v) // \brief Convert a scalar ParamValue to a Rummy::Card. Rummy::Card ParamValueToRummyCard(const std::string &suit, const std::string &name, - const ParameterInput::ParamValue &v) { + const ParameterInput::ParamValue &v) { if (std::holds_alternative(v)) return Rummy::Card(suit, name, std::get(v), ""); if (std::holds_alternative(v)) @@ -386,7 +393,8 @@ Rummy::Card ParamValueToRummyCard(const std::string &suit, const std::string &na std::size_t pos; double d = std::stod(trimmed, &pos); return Rummy::Card(suit, name, d, ""); - } catch (...) {} + } catch (...) { + } return Rummy::Card(suit, name, trimmed, ""); } @@ -407,7 +415,7 @@ void ParameterInput::LoadFromRummyStream(std::istream &is) { const std::string &block_name = suit_name; const auto &suit_cards = deck_->GetCardsInOrder(suit_name); for (const auto &card_name : suit_cards) { - // match for vector + // match for vector if (deck_->IsCardVector(suit_name, card_name)) { std::vector comments; auto elements = deck_->GetVector(suit_name, card_name, comments); @@ -415,7 +423,7 @@ void ParameterInput::LoadFromRummyStream(std::istream &is) { std::string joined_comments; for (std::size_t i = 0; i < elements.size(); ++i) { if (comments[i] != "") { - if (i > 0) { + if (i > 0) { joined_comments += " "; } joined_comments += comments[i]; @@ -533,9 +541,8 @@ void ParameterInput::LoadFromRummyFile(const std::string &filename) { PARTHENON_MPI_CHECK( MPI_Bcast(&content_size, sizeof(std::size_t), MPI_BYTE, 0, MPI_COMM_WORLD)); content.resize(content_size); - PARTHENON_MPI_CHECK( - MPI_Bcast(content.data(), static_cast(content_size), MPI_BYTE, 0, - MPI_COMM_WORLD)); + PARTHENON_MPI_CHECK(MPI_Bcast(content.data(), static_cast(content_size), MPI_BYTE, + 0, MPI_COMM_WORLD)); #else std::ifstream file(filename); PARTHENON_REQUIRE_THROWS(file.is_open(), @@ -671,7 +678,7 @@ void ParameterInput::ModifyFromCmdline(std::vector mods) { std::string input_text, block, name, value; std::stringstream msg; - while(std::getline(ss, input_text)) { + while (std::getline(ss, input_text)) { std::size_t equal_posn = input_text.find_first_of("="); // first "=" character std::size_t slash_posn = input_text.rfind("/", equal_posn); // last "/" before "=" diff --git a/src/parameter_input.hpp b/src/parameter_input.hpp index 17d5c809c3c4f..d8196e86db17f 100644 --- a/src/parameter_input.hpp +++ b/src/parameter_input.hpp @@ -49,7 +49,9 @@ #include "utils/utils.hpp" // Forward-declare Rummy::Deck -namespace Rummy { class Deck; } +namespace Rummy { +class Deck; +} namespace parthenon { @@ -447,7 +449,6 @@ class ParameterInput { InputFormat GetFormat() const { return format; } private: - InputFormat format = InputFormat::Native; bool deck_initialized_ = false; std::unique_ptr deck_; diff --git a/src/parthenon_manager.cpp b/src/parthenon_manager.cpp index 21d1bd4962967..6aa41214a7b2f 100644 --- a/src/parthenon_manager.cpp +++ b/src/parthenon_manager.cpp @@ -123,7 +123,7 @@ ParthenonStatus ParthenonManager::ParthenonInitEnv(int argc, char *argv[]) { if (!arg.input_filenames.empty()) { // Modify info read from restart file if (arg.is_restart) { - for(const auto &input_filename : arg.input_filenames) { + for (const auto &input_filename : arg.input_filenames) { pinput->ReadFile(input_filename, arg.is_restart); } // Populate new object for fresh simulation @@ -134,10 +134,10 @@ ParthenonStatus ParthenonManager::ParthenonInitEnv(int argc, char *argv[]) { } } } - + // Modify based on command line inputs pinput->ModifyFromCmdline(arg.modifiers); - + // Finalize parsing phase - parsers can no longer add parameters pinput->FinalizeParsing(); From a365946a0b23018b4a0c33da61ead44dfe2f70f1 Mon Sep 17 00:00:00 2001 From: Adam Dempsey Date: Tue, 5 May 2026 17:21:01 -0600 Subject: [PATCH 20/46] IsRummyFormat needs to know if this was a command line input --- src/parameter_input.cpp | 26 ++++++++++++++++++-------- src/parameter_input.hpp | 2 +- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/parameter_input.cpp b/src/parameter_input.cpp index a3cc22d525201..e8f173932d4f5 100644 --- a/src/parameter_input.cpp +++ b/src/parameter_input.cpp @@ -261,7 +261,7 @@ void ParameterInput::LoadFromFile(IOWrapper &input) { // - Relative suit paths starting with <.. // - Rummy-specific value syntax: ** power operator, quoted strings, // bracket syntax [ ] (vectors/slices), or slice colon inside brackets -bool ParameterInput::IsRummyFormat(std::istream &is) { +bool ParameterInput::IsRummyFormat(std::istream &is, const bool command_line) { const auto start_pos = is.tellg(); auto restore_and_return = [&](bool result) { is.clear(); @@ -298,31 +298,41 @@ bool ParameterInput::IsRummyFormat(std::istream &is) { } if (line.compare(first_char, 1, "<") == 0) { - if (line.size() > first_char + 2 && line.compare(first_char + 1, 2, "..") == 0) + if (line.size() > first_char + 2 && line.compare(first_char + 1, 2, "..") == 0) { return restore_and_return(true); + } found_block = true; continue; } // Non-comment, non-blank content before the first block = Rummy global variable - if (!found_block) return restore_and_return(true); + // Disable for command line modifications + if (!command_line && !found_block) { + return restore_and_return(true); + } // Rummy-specific syntax in the value part auto eq_pos = line.find('='); if (eq_pos != std::string::npos) { std::string name_part = line.substr(first_char, eq_pos - first_char); - if (name_part.find_first_of(".[") != std::string::npos) + if (name_part.find_first_of(".[") != std::string::npos) { return restore_and_return(true); + } std::string value_part = line.substr(eq_pos + 1); - if (value_part.find_first_of("*\"[+-/%^|") != std::string::npos) + // do not include +- because they can be used in exponential notation. + // / can be used in command line arguments + if (value_part.find_first_of("*\"[%^|") != std::string::npos) { return restore_and_return(true); + } } // Slice syntax on the LHS: name[:2] or name[0:2] std::string lhs = line.substr(first_char, eq_pos == std::string::npos ? std::string::npos : eq_pos - first_char); - if (lhs.find('[') != std::string::npos) return restore_and_return(true); + if (lhs.find('[') != std::string::npos) { + return restore_and_return(true); + } } return restore_and_return(false); } @@ -333,7 +343,7 @@ bool ParameterInput::IsRummyFormat(std::istream &is) { bool ParameterInput::IsRummyFormat(const std::string &filename) { std::ifstream file(filename); if (!file.is_open()) return false; - return IsRummyFormat(file); + return IsRummyFormat(file, false); } //---------------------------------------------------------------------------------------- @@ -664,7 +674,7 @@ void ParameterInput::ModifyFromCmdline(std::vector mods) { ss << mod << "\n"; } - if (format == InputFormat::Rummy || IsRummyFormat(ss)) { + if (format == InputFormat::Rummy || IsRummyFormat(ss, true)) { if (!deck_initialized_) { SyncDeckFromStorage(); deck_initialized_ = true; diff --git a/src/parameter_input.hpp b/src/parameter_input.hpp index d8196e86db17f..c12108e11a277 100644 --- a/src/parameter_input.hpp +++ b/src/parameter_input.hpp @@ -230,7 +230,7 @@ class ParameterInput { void LoadFromRummyFile(const std::string &filename); void LoadFromRummyStream(std::istream &is); void SyncDeckFromStorage(); - static bool IsRummyFormat(std::istream &is); + static bool IsRummyFormat(std::istream &is, const bool command_line); static bool IsRummyFormat(const std::string &filename); void ModifyFromCmdline(std::vector mods); From c0314bd96ac274750de2e00a5aca409bbc86f543 Mon Sep 17 00:00:00 2001 From: Adam Dempsey Date: Wed, 6 May 2026 06:39:17 -0600 Subject: [PATCH 21/46] Format --- .../sparse_advection_rummy.py | 4 +- tst/unit/test_rummy.cpp | 140 ++++++++---------- 2 files changed, 63 insertions(+), 81 deletions(-) diff --git a/tst/regression/test_suites/sparse_advection_rummy/sparse_advection_rummy.py b/tst/regression/test_suites/sparse_advection_rummy/sparse_advection_rummy.py index 98bbb1748cf43..9b9c3a1007bfc 100644 --- a/tst/regression/test_suites/sparse_advection_rummy/sparse_advection_rummy.py +++ b/tst/regression/test_suites/sparse_advection_rummy/sparse_advection_rummy.py @@ -37,7 +37,7 @@ def Prepare(self, parameters, step): if step == 2: parameters.driver_cmd_line_args = [ "parthenon.mesh.nx2=32", - "parthenon.job.problem_id=\"sparse_twotree\"", + 'parthenon.job.problem_id="sparse_twotree"', ] # Run a test with two trees and a statically refined region @@ -45,7 +45,7 @@ def Prepare(self, parameters, step): parameters.driver_cmd_line_args = [ "parthenon.mesh.nx2=32", "parthenon.time.nlim=50", - "parthenon.job.problem_id=\"sparse_twotree_static\"", + 'parthenon.job.problem_id="sparse_twotree_static"', "parthenon.mesh.refinement=static", "parthenon.static_refinement0.x1min=-0.75", "parthenon.static_refinement0.x1max=-0.5", diff --git a/tst/unit/test_rummy.cpp b/tst/unit/test_rummy.cpp index 963046c9572cf..08ee27ee47a9c 100644 --- a/tst/unit/test_rummy.cpp +++ b/tst/unit/test_rummy.cpp @@ -27,17 +27,14 @@ TEST_CASE("LoadFromRummyStream: basic scalar types", "[Rummy]") { GIVEN("A Rummy-format stream with bool, string, and numeric cards") { ParameterInput in; in.SetFormat(parthenon::InputFormat::Rummy); - std::istringstream ss( - "\n" - "nx = 64\n" - "cfl = 0.4\n" - "active = true\n" - "label = \"hydro\"\n"); + std::istringstream ss("\n" + "nx = 64\n" + "cfl = 0.4\n" + "active = true\n" + "label = \"hydro\"\n"); in.LoadFromRummyStream(ss); - THEN("Integer parameter is readable") { - REQUIRE(in.GetInteger("mesh", "nx") == 64); - } + THEN("Integer parameter is readable") { REQUIRE(in.GetInteger("mesh", "nx") == 64); } THEN("Real parameter is readable") { REQUIRE(in.GetReal("mesh", "cfl") == Approx(0.4)); } @@ -47,9 +44,7 @@ TEST_CASE("LoadFromRummyStream: basic scalar types", "[Rummy]") { THEN("String parameter is readable") { REQUIRE(in.GetString("mesh", "label") == "hydro"); } - THEN("Block exists") { - REQUIRE(in.DoesBlockExist("mesh")); - } + THEN("Block exists") { REQUIRE(in.DoesBlockExist("mesh")); } THEN("Parameters exist") { REQUIRE(in.DoesParameterExist("mesh", "nx")); REQUIRE(in.DoesParameterExist("mesh", "cfl")); @@ -61,12 +56,11 @@ TEST_CASE("LoadFromRummyStream: global variables go to '/' block", "[Rummy]") { GIVEN("A Rummy-format stream with global variables") { ParameterInput in; in.SetFormat(parthenon::InputFormat::Rummy); - std::istringstream ss( - "Lx = 1.0\n" - "flag = false\n" - "name = \"global_scope\"\n" - "\n" - "nx = 10\n"); + std::istringstream ss("Lx = 1.0\n" + "flag = false\n" + "name = \"global_scope\"\n" + "\n" + "nx = 10\n"); in.LoadFromRummyStream(ss); THEN("Globals are stored under the '/' block") { @@ -87,10 +81,9 @@ TEST_CASE("LoadFromRummyStream: numeric vector reconstruction", "[Rummy]") { ParameterInput in; in.SetFormat(parthenon::InputFormat::Rummy); - std::istringstream ss( - "\n" - "vals = [1.5, 2.5, 3.5]\n" - "counts = [10, 20, 30]\n"); + std::istringstream ss("\n" + "vals = [1.5, 2.5, 3.5]\n" + "counts = [10, 20, 30]\n"); in.LoadFromRummyStream(ss); THEN("Real vector is reconstructed correctly") { @@ -114,9 +107,8 @@ TEST_CASE("LoadFromRummyStream: string vector reconstruction", "[Rummy]") { GIVEN("A Rummy stream with a vector of strings") { ParameterInput in; in.SetFormat(parthenon::InputFormat::Rummy); - std::istringstream ss( - "\n" - "tags = [\"alpha\", \"beta\", \"gamma\"]\n"); + std::istringstream ss("\n" + "tags = [\"alpha\", \"beta\", \"gamma\"]\n"); in.LoadFromRummyStream(ss); THEN("String vector is reconstructed correctly") { @@ -133,11 +125,10 @@ TEST_CASE("LoadFromRummyStream: expressions are evaluated", "[Rummy]") { GIVEN("A Rummy stream with arithmetic expressions and cross-suit references") { ParameterInput in; in.SetFormat(parthenon::InputFormat::Rummy); - std::istringstream ss( - "base = 4.0\n" - "\n" - "doubled = base * 2.0\n" - "squared = base**2\n"); + std::istringstream ss("base = 4.0\n" + "\n" + "doubled = base * 2.0\n" + "squared = base**2\n"); in.LoadFromRummyStream(ss); THEN("Expressions are fully evaluated before storage") { @@ -260,9 +251,7 @@ TEST_CASE("LoadFromRummyStream: ModifyFromCmdline overrides Rummy params", "[Rum WHEN("ModifyFromCmdline overrides the parameter") { in.ModifyFromCmdline({"mesh.nx = 128"}); - THEN("The override wins") { - REQUIRE(in.GetInteger("mesh", "nx") == 128); - } + THEN("The override wins") { REQUIRE(in.GetInteger("mesh", "nx") == 128); } } } } @@ -271,10 +260,9 @@ TEST_CASE("LoadFromRummyStream: comma-separated vector without brackets", "[Rumm GIVEN("A Rummy stream using bare comma-separated syntax") { ParameterInput in; in.SetFormat(parthenon::InputFormat::Rummy); - std::istringstream ss( - "\n" - "vals = 1.0, 2.0, 3.0\n" - "counts = 10, 20, 30\n"); + std::istringstream ss("\n" + "vals = 1.0, 2.0, 3.0\n" + "counts = 10, 20, 30\n"); in.LoadFromRummyStream(ss); THEN("Real vector is reconstructed correctly") { @@ -298,9 +286,8 @@ TEST_CASE("LoadFromRummyStream: slice assignment syntax", "[Rummy]") { GIVEN("A Rummy stream using slice assignment v[:N] = [...]") { ParameterInput in; in.SetFormat(parthenon::InputFormat::Rummy); - std::istringstream ss( - "\n" - "v[:3] = [100, 200, 300]\n"); + std::istringstream ss("\n" + "v[:3] = [100, 200, 300]\n"); in.LoadFromRummyStream(ss); THEN("Vector is reconstructed correctly from slice assignment") { @@ -317,12 +304,11 @@ TEST_CASE("LoadFromRummyStream: cross-block references are evaluated", "[Rummy]" GIVEN("A Rummy stream where one block references another block's variable") { ParameterInput in; in.SetFormat(parthenon::InputFormat::Rummy); - std::istringstream ss( - "\n" - "gamma = 1.4\n" - "\n" - "gamma_minus_one = physics.gamma - 1.0\n" - "gamma_sq = physics.gamma ** 2\n"); + std::istringstream ss("\n" + "gamma = 1.4\n" + "\n" + "gamma_minus_one = physics.gamma - 1.0\n" + "gamma_sq = physics.gamma ** 2\n"); in.LoadFromRummyStream(ss); THEN("Cross-block reference is fully evaluated before storage") { @@ -336,11 +322,10 @@ TEST_CASE("LoadFromRummyStream: global variables accessible from blocks", "[Rumm GIVEN("A Rummy stream with a global variable used inside a block") { ParameterInput in; in.SetFormat(parthenon::InputFormat::Rummy); - std::istringstream ss( - "Lx = 10.0\n" - "\n" - "dx = Lx / 100\n" - "half_Lx = Lx * 0.5\n"); + std::istringstream ss("Lx = 10.0\n" + "\n" + "dx = Lx / 100\n" + "half_Lx = Lx * 0.5\n"); in.LoadFromRummyStream(ss); THEN("Global is stored under the '/' block") { @@ -359,21 +344,19 @@ TEST_CASE("LoadFromRummyStream: print statement outside a block", "[Rummy]") { in.SetFormat(parthenon::InputFormat::Rummy); // print is a Rummy/pips statement; it produces output but no card. // Verify it doesn't crash and doesn't appear as a parameter. - std::istringstream ss( - "x = 42.0\n" - "print(x)\n" - "\n" - "y = x + 1\n"); + std::istringstream ss("x = 42.0\n" + "print(x)\n" + "\n" + "y = x + 1\n"); THEN("LoadFromRummyStream completes without error") { REQUIRE_NOTHROW(in.LoadFromRummyStream(ss)); } AND_THEN("The print statement produces no stored parameter") { - std::istringstream ss2( - "x = 42.0\n" - "print(x)\n" - "\n" - "y = x + 1\n"); + std::istringstream ss2("x = 42.0\n" + "print(x)\n" + "\n" + "y = x + 1\n"); in.LoadFromRummyStream(ss2); REQUIRE_FALSE(in.DoesParameterExist("/", "print")); REQUIRE(in.GetReal("block", "y") == Approx(43.0)); @@ -382,15 +365,15 @@ TEST_CASE("LoadFromRummyStream: print statement outside a block", "[Rummy]") { } TEST_CASE("LoadFromRummyStream: vector slice with element-wise math", "[Rummy]") { - GIVEN("A Rummy stream that defines a 3-element vector, then cubes a 2-element sub-slice") { + GIVEN("A Rummy stream that defines a 3-element vector, then cubes a 2-element " + "sub-slice") { ParameterInput in; in.SetFormat(parthenon::InputFormat::Rummy); // base[:3] defines [2.0, 3.0, 4.0]. // cubed[:2] = base[:2] ** 3 takes only the first two elements and cubes them. - std::istringstream ss( - "\n" - "base[:3] = [2.0, 3.0, 4.0]\n" - "cubed[:2] = base[:2] ** 3\n"); + std::istringstream ss("\n" + "base[:3] = [2.0, 3.0, 4.0]\n" + "cubed[:2] = base[:2] ** 3\n"); in.LoadFromRummyStream(ss); THEN("Base vector retains all three elements") { @@ -403,29 +386,28 @@ TEST_CASE("LoadFromRummyStream: vector slice with element-wise math", "[Rummy]") THEN("Cubed slice contains only the first two elements, each cubed") { auto c = in.GetVector("block", "cubed"); REQUIRE(c.size() == 2); - REQUIRE(c[0] == Approx(8.0)); // 2^3 - REQUIRE(c[1] == Approx(27.0)); // 3^3 + REQUIRE(c[0] == Approx(8.0)); // 2^3 + REQUIRE(c[1] == Approx(27.0)); // 3^3 } } } -TEST_CASE("LoadFromRummyStream: second stream overwrites existing parameters", "[Rummy]") { +TEST_CASE("LoadFromRummyStream: second stream overwrites existing parameters", + "[Rummy]") { GIVEN("A first Rummy stream establishing initial values") { ParameterInput in; in.SetFormat(parthenon::InputFormat::Rummy); - std::istringstream ss1( - "\n" - "nx = 64\n" - "cfl = 0.3\n" - "\n" - "gamma = 1.4\n"); + std::istringstream ss1("\n" + "nx = 64\n" + "cfl = 0.3\n" + "\n" + "gamma = 1.4\n"); in.LoadFromRummyStream(ss1); WHEN("A second Rummy stream updates some of those parameters") { - std::istringstream ss2( - "\n" - "nx = 128\n" - "cfl = 0.5\n"); + std::istringstream ss2("\n" + "nx = 128\n" + "cfl = 0.5\n"); in.LoadFromRummyStream(ss2); THEN("Updated parameters reflect the second stream") { From 7fbd914b70abf95a66af7bdf69b4c8a1b1a81b58 Mon Sep 17 00:00:00 2001 From: Adam Dempsey Date: Wed, 6 May 2026 08:32:37 -0600 Subject: [PATCH 22/46] Update rummy docs --- doc/sphinx/src/rummy_input.rst | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/doc/sphinx/src/rummy_input.rst b/doc/sphinx/src/rummy_input.rst index 4aa506c25715b..3999fea8d9a4d 100644 --- a/doc/sphinx/src/rummy_input.rst +++ b/doc/sphinx/src/rummy_input.rst @@ -3,10 +3,9 @@ Rummy Input Files ================= -Parthenon supports an extended input file format called **Rummy**, in addition -to the native ``param = value`` format. Rummy retains full backwards -compatibility with native input files while adding expression evaluation, -global variables, vector parameters, relative block paths, file inclusion, +Parthenon supports an extended input file format provided by the `Link Rummy ` library, in addition +to the native, Athena++ format. Rummy input files provide expression evaluation, +global variables, vector operations, relative block paths, file inclusion, and more. Rummy is auto-detected — no special build flag is required. Whether a @@ -36,8 +35,8 @@ so any card defined earlier can be referenced by name in a later expression. Global Variables ---------------- -Cards declared **before** the first ```` header are *global variables*. -They live in the unnamed ``/`` suit and can be referenced by name anywhere in +Variables declared **before** the first ```` header are *global variables*. +Global variables can be referenced by name anywhere in the file without a block qualifier: .. code-block:: text @@ -372,11 +371,9 @@ following is true: 3. A block header begins with ``<..`` (relative path syntax). 4. A parameter **name** contains ``.`` or ``[`` (dotted reference or vector index on the LHS). -5. A parameter **value** contains any of: ``**``, ``"``, ``[``, ``+``, ``-``, - ``/``, ``%``, ``^``, or ``|`` (expression operators or quoted strings). +5. A parameter **value** contains any of: ``**``, ``"``, ``[``, ``%``, ``^``, or ``|`` (expression operators or quoted strings). -To unconditionally use the Rummy parser, place ``# use rummy`` as the very -first line of the file: +To unconditionally use the native (Rummy) parser, place ``# use native`` (``# use rummy``) at the first line of the file: .. code-block:: text From 85f3328245eb587ea59e5b770718407daf30a614 Mon Sep 17 00:00:00 2001 From: Adam Dempsey Date: Wed, 6 May 2026 08:33:47 -0600 Subject: [PATCH 23/46] strip comments before determining if a value is rummyable --- src/parameter_input.cpp | 132 ++++++++++++++++++++-------------------- 1 file changed, 67 insertions(+), 65 deletions(-) diff --git a/src/parameter_input.cpp b/src/parameter_input.cpp index e8f173932d4f5..19ded5de1c35e 100644 --- a/src/parameter_input.cpp +++ b/src/parameter_input.cpp @@ -253,6 +253,71 @@ void ParameterInput::LoadFromFile(IOWrapper &input) { return; } +//---------------------------------------------------------------------------------------- +// Helper functions local to this translation unit for Rummy card conversion + +namespace { + +//! \fn std::string SanitizeString(const std::string &input) +// \brief Strip leading/trailing whitespace and inline comments. +std::string SanitizeString(const std::string &input) { + std::string output = input.substr(0, input.find('#')); // remove trailing comment + output.erase(output.begin(), std::find_if(output.begin(), output.end(), + [](char c) { return !std::isspace(c); })); + output.erase(std::find_if(output.rbegin(), output.rend(), + [](char c) { return !std::isspace(c); }) + .base(), + output.end()); + return output; +} +//! \fn ParameterInput::ParamValue RummyCardToParamValue(const Rummy::Card &card) +// \brief Convert a Rummy Card to a ParameterInput::ParamValue for storage in +// ParameterInput. +ParameterInput::ParamValue RummyCardToParamValue(const Rummy::Card &card) { + if (card.isBool()) { + return card.Get(); + } else if (card.isString()) { + return card.Get(); + } else { + // Otherwise store as UnresolvedString to preserve full precision + return ParameterInput::UnresolvedString( + card.GetString(std::numeric_limits::max_digits10)); + } +} + +//! \fn Rummy::Card ParamValueToRummyCard(suit, name, v) +// \brief Convert a scalar ParamValue to a Rummy::Card. +Rummy::Card ParamValueToRummyCard(const std::string &suit, const std::string &name, + const ParameterInput::ParamValue &v) { + if (std::holds_alternative(v)) + return Rummy::Card(suit, name, std::get(v), ""); + if (std::holds_alternative(v)) + return Rummy::Card(suit, name, static_cast(std::get(v)), ""); + if (std::holds_alternative(v)) + return Rummy::Card(suit, name, static_cast(std::get(v)), ""); + if (std::holds_alternative(v)) + return Rummy::Card(suit, name, std::get(v), ""); + // UnresolvedString + const std::string &raw = std::get(v).value; + std::string trimmed = SanitizeString(raw); + + std::string lower = trimmed; + std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower); + + if (lower == "true") return Rummy::Card(suit, name, true, ""); + if (lower == "false") return Rummy::Card(suit, name, false, ""); + try { + std::size_t pos; + double d = std::stod(trimmed, &pos); + return Rummy::Card(suit, name, d, ""); + } catch (...) { + } + return Rummy::Card(suit, name, trimmed, ""); +} + +} // anonymous namespace + + //---------------------------------------------------------------------------------------- //! \fn bool ParameterInput::IsRummyFormat(std::istream &is) // \brief Detect whether a stream uses Rummy input format by scanning for markers: @@ -289,6 +354,7 @@ bool ParameterInput::IsRummyFormat(std::istream &is, const bool command_line) { if (text_start != std::string::npos) { std::string token = after_hash.substr(text_start); std::transform(token.begin(), token.end(), token.begin(), ::tolower); + if (token.compare(0, 10, "use native") == 0) return restore_and_return(false); if (token.compare(0, 9, "use rummy") == 0) return restore_and_return(true); } continue; @@ -319,7 +385,7 @@ bool ParameterInput::IsRummyFormat(std::istream &is, const bool command_line) { return restore_and_return(true); } - std::string value_part = line.substr(eq_pos + 1); + std::string value_part = SanitizeString(line.substr(eq_pos + 1)); // do not include +- because they can be used in exponential notation. // / can be used in command line arguments if (value_part.find_first_of("*\"[%^|") != std::string::npos) { @@ -346,70 +412,6 @@ bool ParameterInput::IsRummyFormat(const std::string &filename) { return IsRummyFormat(file, false); } -//---------------------------------------------------------------------------------------- -// Helper functions local to this translation unit for Rummy card conversion - -namespace { - -//! \fn std::string SanitizeString(const std::string &input) -// \brief Strip leading/trailing whitespace and inline comments. -std::string SanitizeString(const std::string &input) { - std::string output = input.substr(0, input.find('#')); // remove trailing comment - output.erase(output.begin(), std::find_if(output.begin(), output.end(), - [](char c) { return !std::isspace(c); })); - output.erase(std::find_if(output.rbegin(), output.rend(), - [](char c) { return !std::isspace(c); }) - .base(), - output.end()); - return output; -} -//! \fn ParameterInput::ParamValue RummyCardToParamValue(const Rummy::Card &card) -// \brief Convert a Rummy Card to a ParameterInput::ParamValue for storage in -// ParameterInput. -ParameterInput::ParamValue RummyCardToParamValue(const Rummy::Card &card) { - if (card.isBool()) { - return card.Get(); - } else if (card.isString()) { - return card.Get(); - } else { - // Otherwise store as UnresolvedString to preserve full precision - return ParameterInput::UnresolvedString( - card.GetString(std::numeric_limits::max_digits10)); - } -} - -//! \fn Rummy::Card ParamValueToRummyCard(suit, name, v) -// \brief Convert a scalar ParamValue to a Rummy::Card. -Rummy::Card ParamValueToRummyCard(const std::string &suit, const std::string &name, - const ParameterInput::ParamValue &v) { - if (std::holds_alternative(v)) - return Rummy::Card(suit, name, std::get(v), ""); - if (std::holds_alternative(v)) - return Rummy::Card(suit, name, static_cast(std::get(v)), ""); - if (std::holds_alternative(v)) - return Rummy::Card(suit, name, static_cast(std::get(v)), ""); - if (std::holds_alternative(v)) - return Rummy::Card(suit, name, std::get(v), ""); - // UnresolvedString - const std::string &raw = std::get(v).value; - std::string trimmed = SanitizeString(raw); - - std::string lower = trimmed; - std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower); - - if (lower == "true") return Rummy::Card(suit, name, true, ""); - if (lower == "false") return Rummy::Card(suit, name, false, ""); - try { - std::size_t pos; - double d = std::stod(trimmed, &pos); - return Rummy::Card(suit, name, d, ""); - } catch (...) { - } - return Rummy::Card(suit, name, trimmed, ""); -} - -} // anonymous namespace - //---------------------------------------------------------------------------------------- //! \fn void ParameterInput::LoadFromRummyStream(std::istream &is) // \brief Load parameters from a Rummy-format stream into ParameterInput storage. From ad93b5c8edb1e72469b4e99d1350519e26a02b56 Mon Sep 17 00:00:00 2001 From: Adam Dempsey Date: Wed, 6 May 2026 08:34:51 -0600 Subject: [PATCH 24/46] Fix some issues with rummy test --- tst/regression/CMakeLists.txt | 2 +- .../sparse_advection_rummy/parthinput.sparse_advection_rummy | 2 +- .../sparse_advection_rummy/sparse_advection_rummy.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tst/regression/CMakeLists.txt b/tst/regression/CMakeLists.txt index 00f740d489e08..2c3dfa64ee8b8 100644 --- a/tst/regression/CMakeLists.txt +++ b/tst/regression/CMakeLists.txt @@ -154,7 +154,7 @@ if (ENABLE_HDF5) list(APPEND TEST_DIRS sparse_advection_rummy) list(APPEND TEST_PROCS ${NUM_MPI_PROC_TESTING}) list(APPEND TEST_ARGS "--driver ${PROJECT_BINARY_DIR}/example/sparse_advection/sparse_advection-example \ - --driver_input ${CMAKE_CURRENT_SOURCE_DIR}/test_suites/sparse_advection/parthinput.sparse_advection_rummy \ + --driver_input ${CMAKE_CURRENT_SOURCE_DIR}/test_suites/sparse_advection_rummy/parthinput.sparse_advection_rummy \ --num_steps 3") list(APPEND EXTRA_TEST_LABELS "") diff --git a/tst/regression/test_suites/sparse_advection_rummy/parthinput.sparse_advection_rummy b/tst/regression/test_suites/sparse_advection_rummy/parthinput.sparse_advection_rummy index 710898770ceda..0d863bde54bed 100644 --- a/tst/regression/test_suites/sparse_advection_rummy/parthinput.sparse_advection_rummy +++ b/tst/regression/test_suites/sparse_advection_rummy/parthinput.sparse_advection_rummy @@ -75,7 +75,7 @@ cfl = 0.45 speed = 1.5 refine_tol = 0.3 # control the package specific refinement tagging function -derefine_tol = 0.1 * derefine_tol +derefine_tol = 0.1 * refine_tol file_type = "hdf5" diff --git a/tst/regression/test_suites/sparse_advection_rummy/sparse_advection_rummy.py b/tst/regression/test_suites/sparse_advection_rummy/sparse_advection_rummy.py index 9b9c3a1007bfc..488ada3420a9f 100644 --- a/tst/regression/test_suites/sparse_advection_rummy/sparse_advection_rummy.py +++ b/tst/regression/test_suites/sparse_advection_rummy/sparse_advection_rummy.py @@ -46,7 +46,7 @@ def Prepare(self, parameters, step): "parthenon.mesh.nx2=32", "parthenon.time.nlim=50", 'parthenon.job.problem_id="sparse_twotree_static"', - "parthenon.mesh.refinement=static", + 'parthenon.mesh.refinement="static"', "parthenon.static_refinement0.x1min=-0.75", "parthenon.static_refinement0.x1max=-0.5", "parthenon.static_refinement0.x2min=-0.75", From 5a4a2f802d209688915eb33344db195361639fd5 Mon Sep 17 00:00:00 2001 From: Adam Dempsey Date: Wed, 6 May 2026 10:09:44 -0600 Subject: [PATCH 25/46] Fix compiler warning --- example/sparse_advection/parthenon_app_inputs.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/sparse_advection/parthenon_app_inputs.cpp b/example/sparse_advection/parthenon_app_inputs.cpp index f55e1dfc2aab4..3f4a3868665cb 100644 --- a/example/sparse_advection/parthenon_app_inputs.cpp +++ b/example/sparse_advection/parthenon_app_inputs.cpp @@ -180,7 +180,7 @@ void PostStepDiagnosticsInLoop(Mesh *mesh, ParameterInput *pin, const SimTime &t } std::printf("\n"); Real mem_avg = static_cast(mem_tot) / static_cast(blocks_tot); - std::printf("\tMem used/block in bytes [min, max, avg] = [%llu, %llu, %.14e]\n", + std::printf("\tMem used/block in bytes [min, max, avg] = [%lu, %lu, %.14e]\n", mem_min, mem_max, mem_avg); } } From 5796f43fac1fe31cc1b044976792628c43ab94fa Mon Sep 17 00:00:00 2001 From: Adam Dempsey Date: Wed, 6 May 2026 10:11:45 -0600 Subject: [PATCH 26/46] format --- src/parameter_input.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/parameter_input.cpp b/src/parameter_input.cpp index 19ded5de1c35e..7b10f85218959 100644 --- a/src/parameter_input.cpp +++ b/src/parameter_input.cpp @@ -317,7 +317,6 @@ Rummy::Card ParamValueToRummyCard(const std::string &suit, const std::string &na } // anonymous namespace - //---------------------------------------------------------------------------------------- //! \fn bool ParameterInput::IsRummyFormat(std::istream &is) // \brief Detect whether a stream uses Rummy input format by scanning for markers: From d03daf688e3bd12f5a9f7f54dac3b851569fee0f Mon Sep 17 00:00:00 2001 From: Adam Dempsey Date: Wed, 6 May 2026 10:45:32 -0600 Subject: [PATCH 27/46] Update rummy --- external/rummy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/rummy b/external/rummy index 0a7d9fd80de7b..8e0829e3a9576 160000 --- a/external/rummy +++ b/external/rummy @@ -1 +1 @@ -Subproject commit 0a7d9fd80de7b743390558b666c07ab4a0afb26e +Subproject commit 8e0829e3a957613a0454caa7177af0c52ee66748 From 01f05c5e0717b18ae3eb30127b4366f66cf8d008 Mon Sep 17 00:00:00 2001 From: Adam Dempsey Date: Wed, 6 May 2026 15:08:02 -0600 Subject: [PATCH 28/46] Update rummy --- external/rummy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/rummy b/external/rummy index 8e0829e3a9576..8a0e6f4d5226b 160000 --- a/external/rummy +++ b/external/rummy @@ -1 +1 @@ -Subproject commit 8e0829e3a957613a0454caa7177af0c52ee66748 +Subproject commit 8a0e6f4d5226b4eedf4d3883eaec4c11c3d38e39 From bac544ebe9d5c7d5ce60950287638d03bc0e9cd9 Mon Sep 17 00:00:00 2001 From: Adam Dempsey Date: Wed, 6 May 2026 15:35:00 -0600 Subject: [PATCH 29/46] docs for env vars --- doc/sphinx/src/rummy_input.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/sphinx/src/rummy_input.rst b/doc/sphinx/src/rummy_input.rst index 3999fea8d9a4d..0abfd43f6b0b5 100644 --- a/doc/sphinx/src/rummy_input.rst +++ b/doc/sphinx/src/rummy_input.rst @@ -156,6 +156,11 @@ Almost all ```` functions are available as built-in keywords: lo = min(r, 1.0) +Environment Variables +--------------------- + +The special function ``env("VARIABLE")`` is available for reading environment variables. The output can be stored into a variable for later use. + Ternary Operator ---------------- From 5ed7d48a6dcfce54119826dbfc5be1d9bcc95282 Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 7 May 2026 06:45:07 -0600 Subject: [PATCH 30/46] Update rummy --- external/rummy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/rummy b/external/rummy index 8a0e6f4d5226b..1f031fef9a4bc 160000 --- a/external/rummy +++ b/external/rummy @@ -1 +1 @@ -Subproject commit 8a0e6f4d5226b4eedf4d3883eaec4c11c3d38e39 +Subproject commit 1f031fef9a4bc214a9a6c44310343261a525b2d5 From 49315c699ef0470eb79c3146e42706ca54dbb105 Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 7 May 2026 07:18:27 -0600 Subject: [PATCH 31/46] linter --- src/argument_parser.hpp | 1 + src/parameter_input.cpp | 4 ++-- src/parameter_input.hpp | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/argument_parser.hpp b/src/argument_parser.hpp index 5911279e83016..c8d81a62aa41e 100644 --- a/src/argument_parser.hpp +++ b/src/argument_parser.hpp @@ -21,6 +21,7 @@ #include #include #include +#include #include "defs.hpp" #include "globals.hpp" diff --git a/src/parameter_input.cpp b/src/parameter_input.cpp index 7b10f85218959..1d21b1d9ff90c 100644 --- a/src/parameter_input.cpp +++ b/src/parameter_input.cpp @@ -52,8 +52,6 @@ #include "parameter_input.hpp" -#include "parthenon_mpi.hpp" -#include "rummy/deck.hpp" #include #include @@ -71,6 +69,8 @@ #include #include "globals.hpp" +#include "parthenon_mpi.hpp" +#include "rummy/deck.hpp" #include "utils/error_checking.hpp" namespace parthenon { diff --git a/src/parameter_input.hpp b/src/parameter_input.hpp index c12108e11a277..29e23a8270a5d 100644 --- a/src/parameter_input.hpp +++ b/src/parameter_input.hpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include From 46d69ca9147ae68af34fc0581735fc84dbe7d5fd Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 7 May 2026 07:22:14 -0600 Subject: [PATCH 32/46] format --- src/parameter_input.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/parameter_input.cpp b/src/parameter_input.cpp index 1d21b1d9ff90c..2c6392446ef5c 100644 --- a/src/parameter_input.cpp +++ b/src/parameter_input.cpp @@ -52,7 +52,6 @@ #include "parameter_input.hpp" - #include #include #include From fb736420515110cf1dcdc0ae92fe142810625f38 Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 7 May 2026 07:27:54 -0600 Subject: [PATCH 33/46] Upate rummy --- external/rummy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/rummy b/external/rummy index 1f031fef9a4bc..9127f7dc4f442 160000 --- a/external/rummy +++ b/external/rummy @@ -1 +1 @@ -Subproject commit 1f031fef9a4bc214a9a6c44310343261a525b2d5 +Subproject commit 9127f7dc4f442d547460addc8ad1a9cc06215082 From b144a9cc14dc6c058e580501f2d368dade818607 Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 7 May 2026 07:50:03 -0600 Subject: [PATCH 34/46] Update rummy again --- external/rummy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/rummy b/external/rummy index 9127f7dc4f442..76eea50c59070 160000 --- a/external/rummy +++ b/external/rummy @@ -1 +1 @@ -Subproject commit 9127f7dc4f442d547460addc8ad1a9cc06215082 +Subproject commit 76eea50c59070381e5054b9dde351f4dc7b07d0c From c07162fc5de27ca2a757646a0279aaf6ba273487 Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 7 May 2026 07:50:31 -0600 Subject: [PATCH 35/46] don't install pipslib --- src/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a40b3986859e4..b0e49eea0e2b6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -410,12 +410,12 @@ install(TARGETS parthenon EXPORT parthenonTargets) if(NOT Rummy_FOUND) - install(TARGETS rummylib pipslib EXPORT parthenonTargets + install(TARGETS rummylib EXPORT parthenonTargets RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} ) - install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../external/rummy/rummy + install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../external/rummy/rummy DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} FILES_MATCHING PATTERN "*.hpp" PATTERN "*.h" ) From c207ab78bcd58c7535c23e8e366abb0d041289ae Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 7 May 2026 09:16:06 -0600 Subject: [PATCH 36/46] Update rummy --- external/rummy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/rummy b/external/rummy index 76eea50c59070..12cbbddfbc481 160000 --- a/external/rummy +++ b/external/rummy @@ -1 +1 @@ -Subproject commit 76eea50c59070381e5054b9dde351f4dc7b07d0c +Subproject commit 12cbbddfbc481d99ac8da69bf5d3822fa9c46f88 From 9ee69c044acda55dd0afb80630c6ba86e687f9fb Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 7 May 2026 09:56:37 -0600 Subject: [PATCH 37/46] Update rummy for the last time? --- external/rummy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/rummy b/external/rummy index 12cbbddfbc481..4ddf298c8f010 160000 --- a/external/rummy +++ b/external/rummy @@ -1 +1 @@ -Subproject commit 12cbbddfbc481d99ac8da69bf5d3822fa9c46f88 +Subproject commit 4ddf298c8f010dff2718a5d78ea1f365ffae0ef0 From 1d80fed275b8e65fe6d38ddcde7ebc27f70335c8 Mon Sep 17 00:00:00 2001 From: Adam Dempsey Date: Thu, 7 May 2026 12:47:51 -0600 Subject: [PATCH 38/46] Add from command line comment to those params, but then strip it if using native parser. Make types local as well --- src/parameter_input.cpp | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/parameter_input.cpp b/src/parameter_input.cpp index 2c6392446ef5c..6bea6f8b621ba 100644 --- a/src/parameter_input.cpp +++ b/src/parameter_input.cpp @@ -671,7 +671,7 @@ void ParameterInput::ModifyFromCmdline(std::vector mods) { if (mods.empty()) return; std::stringstream ss; for (const auto &mod : mods) { - ss << mod << "\n"; + ss << mod << " # From command line\n"; } if (format == InputFormat::Rummy || IsRummyFormat(ss, true)) { @@ -685,10 +685,9 @@ void ParameterInput::ModifyFromCmdline(std::vector mods) { } // Native parsing - std::string input_text, block, name, value; - std::stringstream msg; - - while (std::getline(ss, input_text)) { + std::string line; + while (std::getline(ss, line)) { + auto input_text = SanitizeString(line); std::size_t equal_posn = input_text.find_first_of("="); // first "=" character std::size_t slash_posn = input_text.rfind("/", equal_posn); // last "/" before "=" @@ -696,29 +695,32 @@ void ParameterInput::ModifyFromCmdline(std::vector mods) { if ((slash_posn == std::string::npos) || (equal_posn == std::string::npos)) continue; if (slash_posn > equal_posn) { - msg << "'/' used as value (rhs of =) when modifying " << input_text << "." - << " Please update value of change " - << "logic in ModifyFromCmdline function."; + std::stringstream msg << "'/' used as value (rhs of =) when modifying " + << input_text << "." + << " Please update value of change " + << "logic in ModifyFromCmdline function."; PARTHENON_FAIL(msg.str().c_str()); } // extract block/name/value strings - block = input_text.substr(0, slash_posn); - name = input_text.substr(slash_posn + 1, (equal_posn - slash_posn - 1)); - value = input_text.substr(equal_posn + 1, std::string::npos); + auto block = input_text.substr(0, slash_posn); + auto name = input_text.substr(slash_posn + 1, (equal_posn - slash_posn - 1)); + auto value = input_text.substr(equal_posn + 1, std::string::npos); // Check if block/parameter exists for warning messages Block *pb = FindBlock_(block); if (pb == nullptr) { if (Globals::my_rank == 0) { - msg << "In function [ParameterInput::ModifyFromCmdline]:" << std::endl + std::stringstream msg + << "In function [ParameterInput::ModifyFromCmdline]:" << std::endl << " Block name '" << block << "' on command line not found in input/restart file. Block will be added."; PARTHENON_WARN(msg); } } else if (FindParameter_(block, name) == nullptr) { if (Globals::my_rank == 0) { - msg << "In function [ParameterInput::ModifyFromCmdline]:" << std::endl + std::stringstream msg + << "In function [ParameterInput::ModifyFromCmdline]:" << std::endl << " Parameter '" << name << "' in block '" << block << "' on command line not found in input/restart file. Parameter will be " "added."; From 44b9212e26f7f4c8686da8983de0f1552693b81c Mon Sep 17 00:00:00 2001 From: Adam Dempsey Date: Thu, 7 May 2026 12:52:00 -0600 Subject: [PATCH 39/46] Update rummy --- external/rummy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/rummy b/external/rummy index 4ddf298c8f010..6734a06292b70 160000 --- a/external/rummy +++ b/external/rummy @@ -1 +1 @@ -Subproject commit 4ddf298c8f010dff2718a5d78ea1f365ffae0ef0 +Subproject commit 6734a06292b70fc2cbb756e66a073fc7c060013c From 74dfa9b5982659f7fee9a5cf8b9e85c5a7a99595 Mon Sep 17 00:00:00 2001 From: Adam Dempsey Date: Thu, 7 May 2026 12:59:31 -0600 Subject: [PATCH 40/46] Syntax... --- src/parameter_input.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/parameter_input.cpp b/src/parameter_input.cpp index 6bea6f8b621ba..2c4cec51dae02 100644 --- a/src/parameter_input.cpp +++ b/src/parameter_input.cpp @@ -695,10 +695,10 @@ void ParameterInput::ModifyFromCmdline(std::vector mods) { if ((slash_posn == std::string::npos) || (equal_posn == std::string::npos)) continue; if (slash_posn > equal_posn) { - std::stringstream msg << "'/' used as value (rhs of =) when modifying " - << input_text << "." - << " Please update value of change " - << "logic in ModifyFromCmdline function."; + std::stringstream msg; + msg << "'/' used as value (rhs of =) when modifying " << input_text << "." + << " Please update value of change " + << "logic in ModifyFromCmdline function."; PARTHENON_FAIL(msg.str().c_str()); } @@ -711,16 +711,16 @@ void ParameterInput::ModifyFromCmdline(std::vector mods) { Block *pb = FindBlock_(block); if (pb == nullptr) { if (Globals::my_rank == 0) { - std::stringstream msg - << "In function [ParameterInput::ModifyFromCmdline]:" << std::endl + std::stringstream msg; + msg << "In function [ParameterInput::ModifyFromCmdline]:" << std::endl << " Block name '" << block << "' on command line not found in input/restart file. Block will be added."; PARTHENON_WARN(msg); } } else if (FindParameter_(block, name) == nullptr) { if (Globals::my_rank == 0) { - std::stringstream msg - << "In function [ParameterInput::ModifyFromCmdline]:" << std::endl + std::stringstream msg; + msg << "In function [ParameterInput::ModifyFromCmdline]:" << std::endl << " Parameter '" << name << "' in block '" << block << "' on command line not found in input/restart file. Parameter will be " "added."; From babac0066ab46010bbbfbb74c234fd752165c541 Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 16 May 2026 11:28:20 -0600 Subject: [PATCH 41/46] Refactor the rummy interaction to it's own file in paramter_parsers/. Reverts parameter_input to very close to it's original state. Update rummy tests --- src/CMakeLists.txt | 3 + src/parameter_input.cpp | 368 ++----------------------- src/parameter_input.hpp | 21 +- src/parameter_parsers/rummy_parser.cpp | 321 +++++++++++++++++++++ src/parameter_parsers/rummy_parser.hpp | 37 +++ src/parthenon_manager.cpp | 42 ++- tst/unit/test_rummy.cpp | 168 +++++------ 7 files changed, 488 insertions(+), 472 deletions(-) create mode 100644 src/parameter_parsers/rummy_parser.cpp create mode 100644 src/parameter_parsers/rummy_parser.hpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b0e49eea0e2b6..57e8f19c660d9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -239,6 +239,9 @@ add_library(parthenon pack/swarm_pack/swarm_pack_cache.hpp pack/swarm_pack/swarm_pack_types.hpp + parameter_parsers/rummy_parser.cpp + parameter_parsers/rummy_parser.hpp + parthenon/driver.hpp parthenon/package.hpp parthenon/parthenon.hpp diff --git a/src/parameter_input.cpp b/src/parameter_input.cpp index 2c4cec51dae02..4f772862d54a2 100644 --- a/src/parameter_input.cpp +++ b/src/parameter_input.cpp @@ -68,40 +68,36 @@ #include #include "globals.hpp" -#include "parthenon_mpi.hpp" -#include "rummy/deck.hpp" #include "utils/error_checking.hpp" namespace parthenon { +//! \fn std::string SanitizeString(const std::string &input) +// \brief Strip leading/trailing whitespace and inline comments. +std::string SanitizeString(const std::string &input) { + std::string output = input.substr(0, input.find('#')); // remove trailing comment + output.erase(output.begin(), std::find_if(output.begin(), output.end(), + [](char c) { return !std::isspace(c); })); + output.erase(std::find_if(output.rbegin(), output.rend(), + [](char c) { return !std::isspace(c); }) + .base(), + output.end()); + return output; +} //---------------------------------------------------------------------------------------- // ParameterInput constructor -ParameterInput::ParameterInput() - : last_filename_{}, deck_(std::make_unique()) {} +ParameterInput::ParameterInput() : last_filename_{} {} -ParameterInput::ParameterInput(std::string input_filename) - : last_filename_{}, deck_(std::make_unique()) { - ReadFile(input_filename, false); +ParameterInput::ParameterInput(std::string input_filename) : last_filename_{} { + ReadFile(input_filename); } -void ParameterInput::ReadFile(const std::string &input_filename, const bool is_restart) { - if (IsRummyFormat(input_filename)) { - if (is_restart && !deck_initialized_) { - SyncDeckFromStorage(); - } - LoadFromRummyFile(input_filename); - deck_initialized_ = true; - format = InputFormat::Rummy; - } else { - IOWrapper infile; - infile.Open(input_filename.c_str(), IOWrapper::FileMode::read); - LoadFromFile(infile); - infile.Close(); - if (format != InputFormat::Rummy) { - format = InputFormat::Native; - } - } +void ParameterInput::ReadFile(const std::string &input_filename) { + IOWrapper infile; + infile.Open(input_filename.c_str(), IOWrapper::FileMode::read); + LoadFromFile(infile); + infile.Close(); } ParameterInput::~ParameterInput() = default; @@ -252,320 +248,6 @@ void ParameterInput::LoadFromFile(IOWrapper &input) { return; } -//---------------------------------------------------------------------------------------- -// Helper functions local to this translation unit for Rummy card conversion - -namespace { - -//! \fn std::string SanitizeString(const std::string &input) -// \brief Strip leading/trailing whitespace and inline comments. -std::string SanitizeString(const std::string &input) { - std::string output = input.substr(0, input.find('#')); // remove trailing comment - output.erase(output.begin(), std::find_if(output.begin(), output.end(), - [](char c) { return !std::isspace(c); })); - output.erase(std::find_if(output.rbegin(), output.rend(), - [](char c) { return !std::isspace(c); }) - .base(), - output.end()); - return output; -} -//! \fn ParameterInput::ParamValue RummyCardToParamValue(const Rummy::Card &card) -// \brief Convert a Rummy Card to a ParameterInput::ParamValue for storage in -// ParameterInput. -ParameterInput::ParamValue RummyCardToParamValue(const Rummy::Card &card) { - if (card.isBool()) { - return card.Get(); - } else if (card.isString()) { - return card.Get(); - } else { - // Otherwise store as UnresolvedString to preserve full precision - return ParameterInput::UnresolvedString( - card.GetString(std::numeric_limits::max_digits10)); - } -} - -//! \fn Rummy::Card ParamValueToRummyCard(suit, name, v) -// \brief Convert a scalar ParamValue to a Rummy::Card. -Rummy::Card ParamValueToRummyCard(const std::string &suit, const std::string &name, - const ParameterInput::ParamValue &v) { - if (std::holds_alternative(v)) - return Rummy::Card(suit, name, std::get(v), ""); - if (std::holds_alternative(v)) - return Rummy::Card(suit, name, static_cast(std::get(v)), ""); - if (std::holds_alternative(v)) - return Rummy::Card(suit, name, static_cast(std::get(v)), ""); - if (std::holds_alternative(v)) - return Rummy::Card(suit, name, std::get(v), ""); - // UnresolvedString - const std::string &raw = std::get(v).value; - std::string trimmed = SanitizeString(raw); - - std::string lower = trimmed; - std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower); - - if (lower == "true") return Rummy::Card(suit, name, true, ""); - if (lower == "false") return Rummy::Card(suit, name, false, ""); - try { - std::size_t pos; - double d = std::stod(trimmed, &pos); - return Rummy::Card(suit, name, d, ""); - } catch (...) { - } - return Rummy::Card(suit, name, trimmed, ""); -} - -} // anonymous namespace - -//---------------------------------------------------------------------------------------- -//! \fn bool ParameterInput::IsRummyFormat(std::istream &is) -// \brief Detect whether a stream uses Rummy input format by scanning for markers: -// - First line is "# use rummy" (case-insensitive) -// - Non-comment, non-blank content before the first line -// - Relative suit paths starting with <.. -// - Rummy-specific value syntax: ** power operator, quoted strings, -// bracket syntax [ ] (vectors/slices), or slice colon inside brackets -bool ParameterInput::IsRummyFormat(std::istream &is, const bool command_line) { - const auto start_pos = is.tellg(); - auto restore_and_return = [&](bool result) { - is.clear(); - is.seekg(start_pos); - return result; - }; - - bool first_line = true; - bool found_block = false; - std::string line; - while (std::getline(is, line)) { - line.erase(std::remove_if(line.begin(), line.end(), - [](char c) { return std::isspace(c) && c != ' '; }), - line.end()); - if (line.empty()) continue; - auto first_char = line.find_first_not_of(" "); - if (first_char == std::string::npos) continue; - - // Check first non-blank line for "# use rummy" (case-insensitive) - if (first_line) { - first_line = false; - if (line.compare(first_char, 1, "#") == 0) { - std::string after_hash = line.substr(first_char + 1); - auto text_start = after_hash.find_first_not_of(" "); - if (text_start != std::string::npos) { - std::string token = after_hash.substr(text_start); - std::transform(token.begin(), token.end(), token.begin(), ::tolower); - if (token.compare(0, 10, "use native") == 0) return restore_and_return(false); - if (token.compare(0, 9, "use rummy") == 0) return restore_and_return(true); - } - continue; - } - } else { - if (line.compare(first_char, 1, "#") == 0) continue; - } - - if (line.compare(first_char, 1, "<") == 0) { - if (line.size() > first_char + 2 && line.compare(first_char + 1, 2, "..") == 0) { - return restore_and_return(true); - } - found_block = true; - continue; - } - - // Non-comment, non-blank content before the first block = Rummy global variable - // Disable for command line modifications - if (!command_line && !found_block) { - return restore_and_return(true); - } - - // Rummy-specific syntax in the value part - auto eq_pos = line.find('='); - if (eq_pos != std::string::npos) { - std::string name_part = line.substr(first_char, eq_pos - first_char); - if (name_part.find_first_of(".[") != std::string::npos) { - return restore_and_return(true); - } - - std::string value_part = SanitizeString(line.substr(eq_pos + 1)); - // do not include +- because they can be used in exponential notation. - // / can be used in command line arguments - if (value_part.find_first_of("*\"[%^|") != std::string::npos) { - return restore_and_return(true); - } - } - // Slice syntax on the LHS: name[:2] or name[0:2] - std::string lhs = - line.substr(first_char, eq_pos == std::string::npos ? std::string::npos - : eq_pos - first_char); - if (lhs.find('[') != std::string::npos) { - return restore_and_return(true); - } - } - return restore_and_return(false); -} - -//! \fn bool ParameterInput::IsRummyFormat(const std::string &filename) -// \brief Detect whether a file uses Rummy input format. Delegates to the stream -// overload. -bool ParameterInput::IsRummyFormat(const std::string &filename) { - std::ifstream file(filename); - if (!file.is_open()) return false; - return IsRummyFormat(file, false); -} - -//---------------------------------------------------------------------------------------- -//! \fn void ParameterInput::LoadFromRummyStream(std::istream &is) -// \brief Load parameters from a Rummy-format stream into ParameterInput storage. -void ParameterInput::LoadFromRummyStream(std::istream &is) { - PARTHENON_REQUIRE_THROWS(!parsing_finalized_, - "Can't add new parameters after parsing is resolved."); - - deck_->Build(is); - - static const std::regex kVectorCardPattern(R"(^(.+)\[(\d+)\]$)"); - - for (const auto &suit_name : deck_->GetSuitsInOrder()) { - const std::string &block_name = suit_name; - const auto &suit_cards = deck_->GetCardsInOrder(suit_name); - for (const auto &card_name : suit_cards) { - // match for vector - if (deck_->IsCardVector(suit_name, card_name)) { - std::vector comments; - auto elements = deck_->GetVector(suit_name, card_name, comments); - std::string joined; - std::string joined_comments; - for (std::size_t i = 0; i < elements.size(); ++i) { - if (comments[i] != "") { - if (i > 0) { - joined_comments += " "; - } - joined_comments += comments[i]; - } - if (i > 0) { - joined += ","; - } - joined += elements[i]; - } - // Rummy stores comments without '#' - std::string comment; - if (!joined_comments.empty()) comment = "# " + joined_comments; - AddParsedParameter(block_name, card_name, UnresolvedString(joined), comment); - } else { - auto &card = deck_->GetCard(suit_name, card_name); - std::string comment; - if (!card.GetComment().empty()) comment = "# " + card.GetComment(); - AddParsedParameter(block_name, card_name, RummyCardToParamValue(card), comment); - } - } - } -} - -//---------------------------------------------------------------------------------------- -//! \fn void ParameterInput::SyncDeckFromStorage() -// \brief Seed the Rummy Deck from the current param_storage_ contents. -void ParameterInput::SyncDeckFromStorage() { - std::map> new_cards; - std::vector new_suits; - std::map> new_card_map; - - // Register a single card into the three structures, adding the suit on first use. - auto register_card = [&](const std::string &suit, const std::string &card_name, - Rummy::Card card) { - if (new_cards.find(suit) == new_cards.end()) { - new_suits.push_back(suit); - new_card_map[suit] = {}; - } - new_card_map[suit].push_back(card_name); - new_cards[suit][card_name] = std::move(card); - }; - - for (const auto &block : param_storage_) { - // Collapse the block name into a Rummy suit: non-empty '/' segments joined by '/'. - // A block that is only "/" (global scope) maps to suit "/". - std::string suit = "/"; - { - std::string assembled; - std::istringstream bss(block.name); - std::string part; - while (std::getline(bss, part, '/')) { - if (!part.empty()) { - if (!assembled.empty()) assembled += '/'; - assembled += part; - } - } - if (!assembled.empty()) suit = assembled; - } - - for (const auto ¶m : block.params) { - // Vector variants expand to one card per element: name[0], name[1], ... - if (std::holds_alternative>(param.value)) { - const auto &vec = std::get>(param.value); - for (size_t i = 0; i < vec.size(); ++i) { - std::string cn = param.name + "[" + std::to_string(i) + "]"; - register_card(suit, cn, Rummy::Card(suit, cn, static_cast(vec[i]), "")); - } - } else if (std::holds_alternative>(param.value)) { - const auto &vec = std::get>(param.value); - for (size_t i = 0; i < vec.size(); ++i) { - std::string cn = param.name + "[" + std::to_string(i) + "]"; - register_card(suit, cn, Rummy::Card(suit, cn, static_cast(vec[i]), "")); - } - } else if (std::holds_alternative>(param.value)) { - const auto &vec = std::get>(param.value); - for (size_t i = 0; i < vec.size(); ++i) { - std::string cn = param.name + "[" + std::to_string(i) + "]"; - register_card(suit, cn, Rummy::Card(suit, cn, static_cast(vec[i]), "")); - } - } else if (std::holds_alternative>(param.value)) { - const auto &vec = std::get>(param.value); - for (size_t i = 0; i < vec.size(); ++i) { - std::string cn = param.name + "[" + std::to_string(i) + "]"; - register_card(suit, cn, Rummy::Card(suit, cn, vec[i], "")); - } - } else { - register_card(suit, param.name, - ParamValueToRummyCard(suit, param.name, param.value)); - } - } - } - - deck_->SeedGlobals(new_cards, new_suits, new_card_map); -} - -//! \fn void ParameterInput::LoadFromRummyFile(const std::string &filename) -// \brief MPI-safe loader for Rummy-format input files. -void ParameterInput::LoadFromRummyFile(const std::string &filename) { - PARTHENON_REQUIRE_THROWS(!parsing_finalized_, - "Can't add new parameters after parsing is resolved."); - - std::string content; - -#ifdef MPI_PARALLEL - std::size_t content_size = 0; - if (Globals::my_rank == 0) { - std::ifstream file(filename); - PARTHENON_REQUIRE_THROWS(file.is_open(), - "Could not open Rummy input file: " + filename); - std::ostringstream oss; - oss << file.rdbuf(); - content = oss.str(); - content_size = content.size(); - } - PARTHENON_MPI_CHECK( - MPI_Bcast(&content_size, sizeof(std::size_t), MPI_BYTE, 0, MPI_COMM_WORLD)); - content.resize(content_size); - PARTHENON_MPI_CHECK(MPI_Bcast(content.data(), static_cast(content_size), MPI_BYTE, - 0, MPI_COMM_WORLD)); -#else - std::ifstream file(filename); - PARTHENON_REQUIRE_THROWS(file.is_open(), - "Could not open Rummy input file: " + filename); - std::ostringstream oss; - oss << file.rdbuf(); - content = oss.str(); -#endif - - std::istringstream is(content); - LoadFromRummyStream(is); -} - //---------------------------------------------------------------------------------------- //! \fn Block* ParameterInput::FindBlock_(const std::string & name) // \brief find specified Block. Returns pointer to block or nullptr. @@ -674,16 +356,6 @@ void ParameterInput::ModifyFromCmdline(std::vector mods) { ss << mod << " # From command line\n"; } - if (format == InputFormat::Rummy || IsRummyFormat(ss, true)) { - if (!deck_initialized_) { - SyncDeckFromStorage(); - deck_initialized_ = true; - } - LoadFromRummyStream(ss); - format = InputFormat::Rummy; - return; - } - // Native parsing std::string line; while (std::getline(ss, line)) { diff --git a/src/parameter_input.hpp b/src/parameter_input.hpp index 29e23a8270a5d..0c4eab8d8b194 100644 --- a/src/parameter_input.hpp +++ b/src/parameter_input.hpp @@ -28,7 +28,6 @@ #include #include #include -#include #include #include #include @@ -49,14 +48,9 @@ #include "utils/type_list.hpp" #include "utils/utils.hpp" -// Forward-declare Rummy::Deck -namespace Rummy { -class Deck; -} - namespace parthenon { -enum class InputFormat { Native, Rummy, Unknown }; +std::string SanitizeString(const std::string &input); //---------------------------------------------------------------------------------------- // Supported parameter types - single source of truth @@ -223,16 +217,11 @@ class ParameterInput { ParameterInput(); explicit ParameterInput(std::string input_filename); ~ParameterInput(); - void ReadFile(const std::string &input_filename, const bool is_restart); + void ReadFile(const std::string &input_filename); // === PARSING INTERFACE === void LoadFromStream(std::istream &is); void LoadFromFile(IOWrapper &input); - void LoadFromRummyFile(const std::string &filename); - void LoadFromRummyStream(std::istream &is); - void SyncDeckFromStorage(); - static bool IsRummyFormat(std::istream &is, const bool command_line); - static bool IsRummyFormat(const std::string &filename); void ModifyFromCmdline(std::vector mods); // === PARSER INTERFACE (for input sources like text files, Python, TOML, etc.) === @@ -446,13 +435,9 @@ class ParameterInput { return ret; } - void SetFormat(InputFormat fmt) { format = fmt; } - InputFormat GetFormat() const { return format; } + const std::vector &GetBlocks() const { return param_storage_; } private: - InputFormat format = InputFormat::Native; - bool deck_initialized_ = false; - std::unique_ptr deck_; // === PARAMETER STORAGE (vector-of-vectors, preserves insertion order) === std::vector param_storage_; // Ordered storage (for iteration) std::unordered_map diff --git a/src/parameter_parsers/rummy_parser.cpp b/src/parameter_parsers/rummy_parser.cpp new file mode 100644 index 0000000000000..32282da75ca74 --- /dev/null +++ b/src/parameter_parsers/rummy_parser.cpp @@ -0,0 +1,321 @@ +//======================================================================================== +// (C) (or copyright) 2026. Triad National Security, LLC. All rights reserved. +// +// This program was produced under U.S. Government contract 89233218CNA000001 for Los +// Alamos National Laboratory (LANL), which is operated by Triad National Security, LLC +// for the U.S. Department of Energy/National Nuclear Security Administration. All rights +// in the program are reserved by Triad National Security, LLC, and the U.S. Department +// of Energy/National Nuclear Security Administration. The Government is granted for +// itself and others acting on its behalf a nonexclusive, paid-up, irrevocable worldwide +// license in this material to reproduce, prepare derivative works, distribute copies to +// the public, perform publicly and display publicly, and to permit others to do so. +//======================================================================================== +// This file was made in part with generative AI. + +#include +#include +#include +#include +#include + +#include "parameter_input.hpp" +#include "rummy_parser.hpp" +#include + +namespace parthenon { + +//! \fn ParameterInput::ParamValue RummyCardToParamValue(const Rummy::Card &card) +// \brief Convert a Rummy Card to a ParameterInput::ParamValue for storage in +// ParameterInput. +ParamValue RummyCardToParamValue(const Rummy::Card &card) { + if (card.isBool()) { + return card.Get(); + } else if (card.isString()) { + return card.Get(); + } else { + // Otherwise store as UnresolvedString to preserve full precision + return UnresolvedString(card.GetString(std::numeric_limits::max_digits10)); + } +} + +//! \fn Rummy::Card ParamValueToRummyCard(suit, name, v) +// \brief Convert a scalar ParamValue to a Rummy::Card. +Rummy::Card ParamValueToRummyCard(const std::string &suit, const std::string &name, + const ParamValue &v) { + if (std::holds_alternative(v)) + return Rummy::Card(suit, name, std::get(v), ""); + if (std::holds_alternative(v)) + return Rummy::Card(suit, name, static_cast(std::get(v)), ""); + if (std::holds_alternative(v)) + return Rummy::Card(suit, name, static_cast(std::get(v)), ""); + if (std::holds_alternative(v)) + return Rummy::Card(suit, name, std::get(v), ""); + // UnresolvedString + const std::string &raw = std::get(v).value; + std::string trimmed = SanitizeString(raw); + + std::string lower = trimmed; + std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower); + + if (lower == "true") return Rummy::Card(suit, name, true, ""); + if (lower == "false") return Rummy::Card(suit, name, false, ""); + try { + std::size_t pos; + double d = std::stod(trimmed, &pos); + return Rummy::Card(suit, name, d, ""); + } catch (...) { + } + return Rummy::Card(suit, name, trimmed, ""); +} + +void LoadParameterFromRummy(ParameterInput &pin, std::istream &ss, const bool sync) { + Rummy::Deck deck; + if (sync) { + SyncDeckFromStorage(pin, deck); + } + deck.Build(ss); + AddRummyParameters(pin, deck); +} + +void LoadParameterFromRummy(ParameterInput &pin, const std::vector &files, + const std::vector &mods, const bool is_restart) { + Rummy::Deck deck; + + const bool no_inputs = files.empty() && mods.empty(); + if (no_inputs) { + return; + } + + if (is_restart) { + // If this is a restart, we need to sync the deck with the existing parameters + SyncDeckFromStorage(pin, deck); + } + + // concatenate all input files and mods into a single stream for parsing + std::stringstream contents; + for (const auto &file : files) { + std::ifstream input_file(file); + if (input_file.is_open()) { + contents << input_file.rdbuf() << "\n"; + } else { + std::stringstream msg; + msg << "Could not open file '" << file << "'"; + PARTHENON_FAIL(msg); + } + } + for (const auto &mod : mods) { + contents << mod << " # From command line\n"; + } + + deck.Build(contents); + + AddRummyParameters(pin, deck); +} + +void AddRummyParameters(ParameterInput &pin, Rummy::Deck &deck) { + static const std::regex kVectorCardPattern(R"(^(.+)\[(\d+)\]$)"); + + for (const auto &suit_name : deck.GetSuitsInOrder()) { + const std::string &block_name = suit_name; + const auto &suit_cards = deck.GetCardsInOrder(suit_name); + for (const auto &card_name : suit_cards) { + // match for vector + if (deck.IsCardVector(suit_name, card_name)) { + std::vector comments; + auto elements = deck.GetVector(suit_name, card_name, comments); + std::string joined; + std::string joined_comments; + for (std::size_t i = 0; i < elements.size(); ++i) { + if (comments[i] != "") { + if (i > 0) { + joined_comments += " "; + } + joined_comments += comments[i]; + } + if (i > 0) { + joined += ","; + } + joined += elements[i]; + } + // Rummy stores comments without '#' + std::string comment; + if (!joined_comments.empty()) comment = "# " + joined_comments; + pin.AddParsedParameter(block_name, card_name, UnresolvedString(joined), comment); + } else { + auto &card = deck.GetCard(suit_name, card_name); + std::string comment; + if (!card.GetComment().empty()) comment = "# " + card.GetComment(); + pin.AddParsedParameter(block_name, card_name, RummyCardToParamValue(card), + comment); + } + } + } +} + +//---------------------------------------------------------------------------------------- +//! \fn bool IsRummyFormat(std::istream &is) +// \brief Detect whether a stream uses Rummy input format by scanning for markers: +// - First line is "# use rummy" (case-insensitive) +// - Non-comment, non-blank content before the first line +// - Relative suit paths starting with <.. +// - Rummy-specific value syntax: ** power operator, quoted strings, +// bracket syntax [ ] (vectors/slices), or slice colon inside brackets +bool IsRummyFormat(std::istream &is, const bool command_line) { + const auto start_pos = is.tellg(); + auto restore_and_return = [&](bool result) { + is.clear(); + is.seekg(start_pos); + return result; + }; + + bool first_line = true; + bool found_block = false; + std::string line; + while (std::getline(is, line)) { + line.erase(std::remove_if(line.begin(), line.end(), + [](char c) { return std::isspace(c) && c != ' '; }), + line.end()); + if (line.empty()) continue; + auto first_char = line.find_first_not_of(" "); + if (first_char == std::string::npos) continue; + + // Check first non-blank line for "# use rummy" (case-insensitive) + if (first_line) { + first_line = false; + if (line.compare(first_char, 1, "#") == 0) { + std::string after_hash = line.substr(first_char + 1); + auto text_start = after_hash.find_first_not_of(" "); + if (text_start != std::string::npos) { + std::string token = after_hash.substr(text_start); + std::transform(token.begin(), token.end(), token.begin(), ::tolower); + if (token.compare(0, 10, "use native") == 0) return restore_and_return(false); + if (token.compare(0, 9, "use rummy") == 0) return restore_and_return(true); + } + continue; + } + } else { + if (line.compare(first_char, 1, "#") == 0) continue; + } + + if (line.compare(first_char, 1, "<") == 0) { + if (line.size() > first_char + 2 && line.compare(first_char + 1, 2, "..") == 0) { + return restore_and_return(true); + } + found_block = true; + continue; + } + + // Non-comment, non-blank content before the first block = Rummy global variable + // Disable for command line modifications + if (!command_line && !found_block) { + return restore_and_return(true); + } + + // Rummy-specific syntax in the value part + auto eq_pos = line.find('='); + if (eq_pos != std::string::npos) { + std::string name_part = line.substr(first_char, eq_pos - first_char); + if (name_part.find_first_of(".[") != std::string::npos) { + return restore_and_return(true); + } + + std::string value_part = SanitizeString(line.substr(eq_pos + 1)); + // do not include +- because they can be used in exponential notation. + // / can be used in command line arguments + if (value_part.find_first_of("*\"[%^|") != std::string::npos) { + return restore_and_return(true); + } + } + // Slice syntax on the LHS: name[:2] or name[0:2] + std::string lhs = + line.substr(first_char, eq_pos == std::string::npos ? std::string::npos + : eq_pos - first_char); + if (lhs.find('[') != std::string::npos) { + return restore_and_return(true); + } + } + return restore_and_return(false); +} + +//! \fn bool ParameterInput::IsRummyFormat(const std::string &filename) +// \brief Detect whether a file uses Rummy input format. Delegates to the stream +// overload. +bool IsRummyFormat(const std::string &filename) { + std::ifstream file(filename); + if (!file.is_open()) return false; + return IsRummyFormat(file, false); +} + +//---------------------------------------------------------------------------------------- +//! \fn void ParameterInput::SyncDeckFromStorage() +// \brief Seed the Rummy Deck from the current param_storage_ contents. +void SyncDeckFromStorage(ParameterInput &pin, Rummy::Deck &deck) { + std::map> new_cards; + std::vector new_suits; + std::map> new_card_map; + + // Register a single card into the three structures, adding the suit on first use. + auto register_card = [&](const std::string &suit, const std::string &card_name, + Rummy::Card card) { + if (new_cards.find(suit) == new_cards.end()) { + new_suits.push_back(suit); + new_card_map[suit] = {}; + } + new_card_map[suit].push_back(card_name); + new_cards[suit][card_name] = std::move(card); + }; + + for (const auto &block : pin.GetBlocks()) { + // Collapse the block name into a Rummy suit: non-empty '/' segments joined by '/'. + // A block that is only "/" (global scope) maps to suit "/". + std::string suit = "/"; + { + std::string assembled; + std::istringstream bss(block.name); + std::string part; + while (std::getline(bss, part, '/')) { + if (!part.empty()) { + if (!assembled.empty()) assembled += '/'; + assembled += part; + } + } + if (!assembled.empty()) suit = assembled; + } + + for (const auto ¶m : block.params) { + // Vector variants expand to one card per element: name[0], name[1], ... + if (std::holds_alternative>(param.value)) { + const auto &vec = std::get>(param.value); + for (size_t i = 0; i < vec.size(); ++i) { + std::string cn = param.name + "[" + std::to_string(i) + "]"; + register_card(suit, cn, Rummy::Card(suit, cn, static_cast(vec[i]), "")); + } + } else if (std::holds_alternative>(param.value)) { + const auto &vec = std::get>(param.value); + for (size_t i = 0; i < vec.size(); ++i) { + std::string cn = param.name + "[" + std::to_string(i) + "]"; + register_card(suit, cn, Rummy::Card(suit, cn, static_cast(vec[i]), "")); + } + } else if (std::holds_alternative>(param.value)) { + const auto &vec = std::get>(param.value); + for (size_t i = 0; i < vec.size(); ++i) { + std::string cn = param.name + "[" + std::to_string(i) + "]"; + register_card(suit, cn, Rummy::Card(suit, cn, static_cast(vec[i]), "")); + } + } else if (std::holds_alternative>(param.value)) { + const auto &vec = std::get>(param.value); + for (size_t i = 0; i < vec.size(); ++i) { + std::string cn = param.name + "[" + std::to_string(i) + "]"; + register_card(suit, cn, Rummy::Card(suit, cn, vec[i], "")); + } + } else { + register_card(suit, param.name, + ParamValueToRummyCard(suit, param.name, param.value)); + } + } + } + + deck.SeedGlobals(new_cards, new_suits, new_card_map); +} + +} // namespace parthenon \ No newline at end of file diff --git a/src/parameter_parsers/rummy_parser.hpp b/src/parameter_parsers/rummy_parser.hpp new file mode 100644 index 0000000000000..6e67a3ffdaa34 --- /dev/null +++ b/src/parameter_parsers/rummy_parser.hpp @@ -0,0 +1,37 @@ +//======================================================================================== +// (C) (or copyright) 2026. Triad National Security, LLC. All rights reserved. +// +// This program was produced under U.S. Government contract 89233218CNA000001 for Los +// Alamos National Laboratory (LANL), which is operated by Triad National Security, LLC +// for the U.S. Department of Energy/National Nuclear Security Administration. All rights +// in the program are reserved by Triad National Security, LLC, and the U.S. Department +// of Energy/National Nuclear Security Administration. The Government is granted for +// itself and others acting on its behalf a nonexclusive, paid-up, irrevocable worldwide +// license in this material to reproduce, prepare derivative works, distribute copies to +// the public, perform publicly and display publicly, and to permit others to do so. +//======================================================================================== +// This file was made in part with generative AI. + +#ifndef PARAMETER_PARSERS_RUMMY_PARSER_HPP_ +#define PARAMETER_PARSERS_RUMMY_PARSER_HPP_ + +#include +#include +#include + +#include "parameter_input.hpp" + +// Foward declare Rummy::Deck to avoid including the full header in this file +namespace Rummy { +class Deck; +} +namespace parthenon { +void LoadParameterFromRummy(ParameterInput &input, const std::vector &files, + const std::vector &mods, const bool is_restart); +void LoadParameterFromRummy(ParameterInput &pin, std::istream &ss, const bool sync); +void AddRummyParameters(ParameterInput &pin, Rummy::Deck &deck); +void SyncDeckFromStorage(ParameterInput &pin, Rummy::Deck &deck); +bool IsRummyFormat(const std::string &filename); +bool IsRummyFormat(std::istream &is, const bool command_line); +} // namespace parthenon +#endif // PARAMETER_PARSERS_RUMMY_PARSER_HPP_ \ No newline at end of file diff --git a/src/parthenon_manager.cpp b/src/parthenon_manager.cpp index 6aa41214a7b2f..9869e03112ba1 100644 --- a/src/parthenon_manager.cpp +++ b/src/parthenon_manager.cpp @@ -42,6 +42,7 @@ #include "outputs/outputs_package.hpp" #include "outputs/restart.hpp" #include "outputs/restart_hdf5.hpp" +#include "parameter_parsers/rummy_parser.hpp" #include "utils/error_checking.hpp" #include "utils/utils.hpp" @@ -119,24 +120,37 @@ ParthenonStatus ParthenonManager::ParthenonInitEnv(int argc, char *argv[]) { std::istringstream is(inputString); pinput->LoadFromStream(is); } - // If an input file was provided - if (!arg.input_filenames.empty()) { - // Modify info read from restart file - if (arg.is_restart) { - for (const auto &input_filename : arg.input_filenames) { - pinput->ReadFile(input_filename, arg.is_restart); - } - // Populate new object for fresh simulation - } else { - pinput = std::make_unique(); - for (const auto &input_filename : arg.input_filenames) { - pinput->ReadFile(input_filename, arg.is_restart); + // Determine what parser to use + bool is_rummy = false; + for (const auto &input_filename : arg.input_filenames) { + if (IsRummyFormat(input_filename)) { + is_rummy = true; + break; + } + } + if (!is_rummy) { + for (const auto &mod : arg.modifiers) { + std::stringstream ss(mod); + if (IsRummyFormat(ss, true)) { + is_rummy = true; + break; } } } - // Modify based on command line inputs - pinput->ModifyFromCmdline(arg.modifiers); + // read the parameters + if (!arg.is_restart) { + pinput = std::make_unique(); + } + if (is_rummy) { + LoadParameterFromRummy(*pinput, arg.input_filenames, arg.modifiers, arg.is_restart); + } else { + for (const auto &input_filename : arg.input_filenames) { + pinput->ReadFile(input_filename); + } + // Modify based on command line inputs + pinput->ModifyFromCmdline(arg.modifiers); + } // Finalize parsing phase - parsers can no longer add parameters pinput->FinalizeParsing(); diff --git a/tst/unit/test_rummy.cpp b/tst/unit/test_rummy.cpp index 08ee27ee47a9c..a1db55d9c8e76 100644 --- a/tst/unit/test_rummy.cpp +++ b/tst/unit/test_rummy.cpp @@ -11,7 +11,6 @@ // the public, perform publicly and display publicly, and to permit others to do so. //======================================================================================== -#include #include #include #include @@ -20,19 +19,19 @@ #include #include "parameter_input.hpp" +#include "parameter_parsers/rummy_parser.hpp" using parthenon::ParameterInput; TEST_CASE("LoadFromRummyStream: basic scalar types", "[Rummy]") { GIVEN("A Rummy-format stream with bool, string, and numeric cards") { ParameterInput in; - in.SetFormat(parthenon::InputFormat::Rummy); std::istringstream ss("\n" "nx = 64\n" "cfl = 0.4\n" "active = true\n" "label = \"hydro\"\n"); - in.LoadFromRummyStream(ss); + parthenon::LoadParameterFromRummy(in, ss, false); THEN("Integer parameter is readable") { REQUIRE(in.GetInteger("mesh", "nx") == 64); } THEN("Real parameter is readable") { @@ -55,13 +54,12 @@ TEST_CASE("LoadFromRummyStream: basic scalar types", "[Rummy]") { TEST_CASE("LoadFromRummyStream: global variables go to '/' block", "[Rummy]") { GIVEN("A Rummy-format stream with global variables") { ParameterInput in; - in.SetFormat(parthenon::InputFormat::Rummy); std::istringstream ss("Lx = 1.0\n" "flag = false\n" "name = \"global_scope\"\n" "\n" "nx = 10\n"); - in.LoadFromRummyStream(ss); + parthenon::LoadParameterFromRummy(in, ss, false); THEN("Globals are stored under the '/' block") { REQUIRE(in.DoesParameterExist("/", "Lx")); @@ -79,12 +77,11 @@ TEST_CASE("LoadFromRummyStream: global variables go to '/' block", "[Rummy]") { TEST_CASE("LoadFromRummyStream: numeric vector reconstruction", "[Rummy]") { GIVEN("A Rummy stream with a vector of reals and a vector of ints") { ParameterInput in; - in.SetFormat(parthenon::InputFormat::Rummy); std::istringstream ss("\n" "vals = [1.5, 2.5, 3.5]\n" "counts = [10, 20, 30]\n"); - in.LoadFromRummyStream(ss); + parthenon::LoadParameterFromRummy(in, ss, false); THEN("Real vector is reconstructed correctly") { auto v = in.GetVector("block", "vals"); @@ -106,10 +103,9 @@ TEST_CASE("LoadFromRummyStream: numeric vector reconstruction", "[Rummy]") { TEST_CASE("LoadFromRummyStream: string vector reconstruction", "[Rummy]") { GIVEN("A Rummy stream with a vector of strings") { ParameterInput in; - in.SetFormat(parthenon::InputFormat::Rummy); std::istringstream ss("\n" "tags = [\"alpha\", \"beta\", \"gamma\"]\n"); - in.LoadFromRummyStream(ss); + parthenon::LoadParameterFromRummy(in, ss, false); THEN("String vector is reconstructed correctly") { auto v = in.GetVector("block", "tags"); @@ -124,12 +120,11 @@ TEST_CASE("LoadFromRummyStream: string vector reconstruction", "[Rummy]") { TEST_CASE("LoadFromRummyStream: expressions are evaluated", "[Rummy]") { GIVEN("A Rummy stream with arithmetic expressions and cross-suit references") { ParameterInput in; - in.SetFormat(parthenon::InputFormat::Rummy); std::istringstream ss("base = 4.0\n" "\n" "doubled = base * 2.0\n" "squared = base**2\n"); - in.LoadFromRummyStream(ss); + parthenon::LoadParameterFromRummy(in, ss, false); THEN("Expressions are fully evaluated before storage") { REQUIRE(in.GetReal("block", "doubled") == Approx(8.0)); @@ -140,103 +135,71 @@ TEST_CASE("LoadFromRummyStream: expressions are evaluated", "[Rummy]") { TEST_CASE("IsRummyFormat: detects Rummy vs legacy format", "[Rummy]") { GIVEN("A legacy-format input file (block header before any value)") { - std::string tmpfile = "/tmp/parthenon_test_legacy.pin"; - { - std::ofstream f(tmpfile); - f << "\n" - << "nx1 = 64\n" - << "nx2 = 32\n"; - } + std::istringstream ss("\n" + "nx1 = 64\n" + "nx2 = 32\n"); THEN("IsRummyFormat returns false") { - REQUIRE(ParameterInput::IsRummyFormat(tmpfile) == false); + REQUIRE(parthenon::IsRummyFormat(ss, false) == false); } } GIVEN("A Rummy-format file: global variable before first block") { - std::string tmpfile = "/tmp/parthenon_test_rummy_global.pin"; - { - std::ofstream f(tmpfile); - f << "Lx = 1.0\n" - << "\n" - << "nx = 64\n"; - } + std::istringstream ss("Lx = 1.0\n" + "\n" + "nx = 64\n"); THEN("IsRummyFormat returns true") { - REQUIRE(ParameterInput::IsRummyFormat(tmpfile) == true); + REQUIRE(parthenon::IsRummyFormat(ss, false) == true); } } GIVEN("A Rummy-format file: relative suit path <../") { - std::string tmpfile = "/tmp/parthenon_test_rummy_relpath.pin"; - { - std::ofstream f(tmpfile); - f << "\n" - << "hydro = true\n" - << "<../eos>\n" - << "gamma = 1.4\n"; - } + std::istringstream ss("\n" + "hydro = true\n" + "<../eos>\n" + "gamma = 1.4\n"); THEN("IsRummyFormat returns true") { - REQUIRE(ParameterInput::IsRummyFormat(tmpfile) == true); + REQUIRE(parthenon::IsRummyFormat(ss, false) == true); } } GIVEN("A Rummy-format file: ** power operator in a value") { - std::string tmpfile = "/tmp/parthenon_test_rummy_power.pin"; - { - std::ofstream f(tmpfile); - f << "\n" - << "val = 2**10\n"; - } + std::istringstream ss("\n" + "val = 2**10\n"); THEN("IsRummyFormat returns true") { - REQUIRE(ParameterInput::IsRummyFormat(tmpfile) == true); + REQUIRE(parthenon::IsRummyFormat(ss, false) == true); } } GIVEN("A Rummy-format file: first line is '# use rummy'") { - std::string tmpfile = "/tmp/parthenon_test_rummy_userummy.pin"; - { - std::ofstream f(tmpfile); - f << "# Use Rummy\n" - << "\n" - << "nx = 64\n"; - } + std::istringstream ss("# Use Rummy\n" + "\n" + "nx = 64\n"); THEN("IsRummyFormat returns true") { - REQUIRE(ParameterInput::IsRummyFormat(tmpfile) == true); + REQUIRE(parthenon::IsRummyFormat(ss, false) == true); } } GIVEN("A Rummy-format file: quoted string value") { - std::string tmpfile = "/tmp/parthenon_test_rummy_quoted.pin"; - { - std::ofstream f(tmpfile); - f << "\n" - << "label = \"hydro\"\n"; - } + std::istringstream ss("\n" + "label = \"hydro\"\n"); THEN("IsRummyFormat returns true") { - REQUIRE(ParameterInput::IsRummyFormat(tmpfile) == true); + REQUIRE(parthenon::IsRummyFormat(ss, false) == true); } } GIVEN("A Rummy-format file: bracket vector syntax in a value") { - std::string tmpfile = "/tmp/parthenon_test_rummy_vec.pin"; - { - std::ofstream f(tmpfile); - f << "\n" - << "nx = [64, 32, 16]\n"; - } + std::istringstream ss("\n" + "nx = [64, 32, 16]\n"); THEN("IsRummyFormat returns true") { - REQUIRE(ParameterInput::IsRummyFormat(tmpfile) == true); + REQUIRE(parthenon::IsRummyFormat(ss, false) == true); } } GIVEN("A Rummy-format file: bracket slice syntax on the LHS") { - std::string tmpfile = "/tmp/parthenon_test_rummy_slice.pin"; - { - std::ofstream f(tmpfile); - f << "\n" - << "nx[:2] = [64, 32]\n"; - } + std::istringstream ss("\n" + "nx[:2] = [64, 32]\n"); THEN("IsRummyFormat returns true") { - REQUIRE(ParameterInput::IsRummyFormat(tmpfile) == true); + REQUIRE(parthenon::IsRummyFormat(ss, false) == true); } } } @@ -244,13 +207,12 @@ TEST_CASE("IsRummyFormat: detects Rummy vs legacy format", "[Rummy]") { TEST_CASE("LoadFromRummyStream: ModifyFromCmdline overrides Rummy params", "[Rummy]") { GIVEN("A Rummy stream with a parameter") { ParameterInput in; - in.SetFormat(parthenon::InputFormat::Rummy); std::istringstream ss("\nnx = 32\n"); - in.LoadFromRummyStream(ss); + parthenon::LoadParameterFromRummy(in, ss, false); WHEN("ModifyFromCmdline overrides the parameter") { - in.ModifyFromCmdline({"mesh.nx = 128"}); - + std::istringstream ss2("mesh.nx = 128\n"); + parthenon::LoadParameterFromRummy(in, ss2, true); THEN("The override wins") { REQUIRE(in.GetInteger("mesh", "nx") == 128); } } } @@ -259,11 +221,10 @@ TEST_CASE("LoadFromRummyStream: ModifyFromCmdline overrides Rummy params", "[Rum TEST_CASE("LoadFromRummyStream: comma-separated vector without brackets", "[Rummy]") { GIVEN("A Rummy stream using bare comma-separated syntax") { ParameterInput in; - in.SetFormat(parthenon::InputFormat::Rummy); std::istringstream ss("\n" "vals = 1.0, 2.0, 3.0\n" "counts = 10, 20, 30\n"); - in.LoadFromRummyStream(ss); + parthenon::LoadParameterFromRummy(in, ss, false); THEN("Real vector is reconstructed correctly") { auto v = in.GetVector("block", "vals"); @@ -285,10 +246,9 @@ TEST_CASE("LoadFromRummyStream: comma-separated vector without brackets", "[Rumm TEST_CASE("LoadFromRummyStream: slice assignment syntax", "[Rummy]") { GIVEN("A Rummy stream using slice assignment v[:N] = [...]") { ParameterInput in; - in.SetFormat(parthenon::InputFormat::Rummy); std::istringstream ss("\n" "v[:3] = [100, 200, 300]\n"); - in.LoadFromRummyStream(ss); + parthenon::LoadParameterFromRummy(in, ss, false); THEN("Vector is reconstructed correctly from slice assignment") { auto v = in.GetVector("block", "v"); @@ -303,13 +263,12 @@ TEST_CASE("LoadFromRummyStream: slice assignment syntax", "[Rummy]") { TEST_CASE("LoadFromRummyStream: cross-block references are evaluated", "[Rummy]") { GIVEN("A Rummy stream where one block references another block's variable") { ParameterInput in; - in.SetFormat(parthenon::InputFormat::Rummy); std::istringstream ss("\n" "gamma = 1.4\n" "\n" "gamma_minus_one = physics.gamma - 1.0\n" "gamma_sq = physics.gamma ** 2\n"); - in.LoadFromRummyStream(ss); + LoadParameterFromRummy(in, ss, false); THEN("Cross-block reference is fully evaluated before storage") { REQUIRE(in.GetReal("eos", "gamma_minus_one") == Approx(0.4)); @@ -321,12 +280,11 @@ TEST_CASE("LoadFromRummyStream: cross-block references are evaluated", "[Rummy]" TEST_CASE("LoadFromRummyStream: global variables accessible from blocks", "[Rummy]") { GIVEN("A Rummy stream with a global variable used inside a block") { ParameterInput in; - in.SetFormat(parthenon::InputFormat::Rummy); std::istringstream ss("Lx = 10.0\n" "\n" "dx = Lx / 100\n" "half_Lx = Lx * 0.5\n"); - in.LoadFromRummyStream(ss); + parthenon::LoadParameterFromRummy(in, ss, false); THEN("Global is stored under the '/' block") { REQUIRE(in.GetReal("/", "Lx") == Approx(10.0)); @@ -338,28 +296,56 @@ TEST_CASE("LoadFromRummyStream: global variables accessible from blocks", "[Rumm } } +static std::string captureStdout(std::function f) { + int pipefd[2]; + pipe(pipefd); + int saved = dup(STDOUT_FILENO); + dup2(pipefd[1], STDOUT_FILENO); + close(pipefd[1]); + + f(); + fflush(stdout); + + dup2(saved, STDOUT_FILENO); + close(saved); + + std::string result; + char buf[256]; + ssize_t n; + while ((n = read(pipefd[0], buf, sizeof(buf))) > 0) + result.append(buf, n); + close(pipefd[0]); + return result; +} + TEST_CASE("LoadFromRummyStream: print statement outside a block", "[Rummy]") { GIVEN("A Rummy stream with a print statement before any block") { ParameterInput in; - in.SetFormat(parthenon::InputFormat::Rummy); // print is a Rummy/pips statement; it produces output but no card. // Verify it doesn't crash and doesn't appear as a parameter. + + // capture stdout to verify print statement doesn't produce stored parameter but does + // produce output + std::istringstream ss("x = 42.0\n" "print(x)\n" "\n" "y = x + 1\n"); THEN("LoadFromRummyStream completes without error") { - REQUIRE_NOTHROW(in.LoadFromRummyStream(ss)); + REQUIRE_NOTHROW(LoadParameterFromRummy(in, ss, false)); } AND_THEN("The print statement produces no stored parameter") { std::istringstream ss2("x = 42.0\n" "print(x)\n" "\n" "y = x + 1\n"); - in.LoadFromRummyStream(ss2); + + std::string dummy_cout = + captureStdout([&]() { parthenon::LoadParameterFromRummy(in, ss2, false); }); REQUIRE_FALSE(in.DoesParameterExist("/", "print")); REQUIRE(in.GetReal("block", "y") == Approx(43.0)); + REQUIRE(dummy_cout.substr(0, 2) == "42"); } } } @@ -368,13 +354,12 @@ TEST_CASE("LoadFromRummyStream: vector slice with element-wise math", "[Rummy]") GIVEN("A Rummy stream that defines a 3-element vector, then cubes a 2-element " "sub-slice") { ParameterInput in; - in.SetFormat(parthenon::InputFormat::Rummy); // base[:3] defines [2.0, 3.0, 4.0]. // cubed[:2] = base[:2] ** 3 takes only the first two elements and cubes them. std::istringstream ss("\n" "base[:3] = [2.0, 3.0, 4.0]\n" "cubed[:2] = base[:2] ** 3\n"); - in.LoadFromRummyStream(ss); + parthenon::LoadParameterFromRummy(in, ss, false); THEN("Base vector retains all three elements") { auto b = in.GetVector("block", "base"); @@ -396,19 +381,18 @@ TEST_CASE("LoadFromRummyStream: second stream overwrites existing parameters", "[Rummy]") { GIVEN("A first Rummy stream establishing initial values") { ParameterInput in; - in.SetFormat(parthenon::InputFormat::Rummy); std::istringstream ss1("\n" "nx = 64\n" "cfl = 0.3\n" "\n" "gamma = 1.4\n"); - in.LoadFromRummyStream(ss1); + parthenon::LoadParameterFromRummy(in, ss1, false); WHEN("A second Rummy stream updates some of those parameters") { std::istringstream ss2("\n" "nx = 128\n" "cfl = 0.5\n"); - in.LoadFromRummyStream(ss2); + parthenon::LoadParameterFromRummy(in, ss2, true); THEN("Updated parameters reflect the second stream") { REQUIRE(in.GetInteger("mesh", "nx") == 128); From 5f8ff153af7c3b8dad8efe9e9fbfc92f4e5a79e5 Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 16 May 2026 15:23:07 -0600 Subject: [PATCH 42/46] cpplint --- src/parameter_parsers/rummy_parser.cpp | 8 ++++++-- src/parameter_parsers/rummy_parser.hpp | 2 +- tst/unit/test_rummy.cpp | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/parameter_parsers/rummy_parser.cpp b/src/parameter_parsers/rummy_parser.cpp index 32282da75ca74..16200a628559e 100644 --- a/src/parameter_parsers/rummy_parser.cpp +++ b/src/parameter_parsers/rummy_parser.cpp @@ -12,15 +12,19 @@ //======================================================================================== // This file was made in part with generative AI. +#include #include +#include #include #include #include +#include #include +#include + #include "parameter_input.hpp" #include "rummy_parser.hpp" -#include namespace parthenon { @@ -318,4 +322,4 @@ void SyncDeckFromStorage(ParameterInput &pin, Rummy::Deck &deck) { deck.SeedGlobals(new_cards, new_suits, new_card_map); } -} // namespace parthenon \ No newline at end of file +} // namespace parthenon diff --git a/src/parameter_parsers/rummy_parser.hpp b/src/parameter_parsers/rummy_parser.hpp index 6e67a3ffdaa34..890b3f654a35d 100644 --- a/src/parameter_parsers/rummy_parser.hpp +++ b/src/parameter_parsers/rummy_parser.hpp @@ -34,4 +34,4 @@ void SyncDeckFromStorage(ParameterInput &pin, Rummy::Deck &deck); bool IsRummyFormat(const std::string &filename); bool IsRummyFormat(std::istream &is, const bool command_line); } // namespace parthenon -#endif // PARAMETER_PARSERS_RUMMY_PARSER_HPP_ \ No newline at end of file +#endif // PARAMETER_PARSERS_RUMMY_PARSER_HPP_ diff --git a/tst/unit/test_rummy.cpp b/tst/unit/test_rummy.cpp index a1db55d9c8e76..bfd7af7e3260f 100644 --- a/tst/unit/test_rummy.cpp +++ b/tst/unit/test_rummy.cpp @@ -11,6 +11,7 @@ // the public, perform publicly and display publicly, and to permit others to do so. //======================================================================================== +#include #include #include #include From 5699cd46f28374da84e36e331fa87ca70e499f0d Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 16 May 2026 17:38:29 -0600 Subject: [PATCH 43/46] add unistd header --- tst/unit/test_rummy.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tst/unit/test_rummy.cpp b/tst/unit/test_rummy.cpp index bfd7af7e3260f..35ae4fe9ae515 100644 --- a/tst/unit/test_rummy.cpp +++ b/tst/unit/test_rummy.cpp @@ -11,7 +11,10 @@ // the public, perform publicly and display publicly, and to permit others to do so. //======================================================================================== +#include + #include +#include #include #include #include From f403c400a90d6382392871e82d35bc55d700e098 Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 16 May 2026 20:23:02 -0600 Subject: [PATCH 44/46] don't try to capture stdout not very portable --- tst/unit/test_rummy.cpp | 33 +-------------------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/tst/unit/test_rummy.cpp b/tst/unit/test_rummy.cpp index 35ae4fe9ae515..95c7a18a31d92 100644 --- a/tst/unit/test_rummy.cpp +++ b/tst/unit/test_rummy.cpp @@ -1,5 +1,5 @@ //======================================================================================== -// (C) (or copyright) 2020-2026. Triad National Security, LLC. All rights reserved. +// (C) (or copyright) 2026. Triad National Security, LLC. All rights reserved. // // This program was produced under U.S. Government contract 89233218CNA000001 for Los // Alamos National Laboratory (LANL), which is operated by Triad National Security, LLC @@ -11,10 +11,7 @@ // the public, perform publicly and display publicly, and to permit others to do so. //======================================================================================== -#include - #include -#include #include #include #include @@ -300,37 +297,12 @@ TEST_CASE("LoadFromRummyStream: global variables accessible from blocks", "[Rumm } } -static std::string captureStdout(std::function f) { - int pipefd[2]; - pipe(pipefd); - int saved = dup(STDOUT_FILENO); - dup2(pipefd[1], STDOUT_FILENO); - close(pipefd[1]); - - f(); - fflush(stdout); - - dup2(saved, STDOUT_FILENO); - close(saved); - - std::string result; - char buf[256]; - ssize_t n; - while ((n = read(pipefd[0], buf, sizeof(buf))) > 0) - result.append(buf, n); - close(pipefd[0]); - return result; -} - TEST_CASE("LoadFromRummyStream: print statement outside a block", "[Rummy]") { GIVEN("A Rummy stream with a print statement before any block") { ParameterInput in; // print is a Rummy/pips statement; it produces output but no card. // Verify it doesn't crash and doesn't appear as a parameter. - // capture stdout to verify print statement doesn't produce stored parameter but does - // produce output - std::istringstream ss("x = 42.0\n" "print(x)\n" "\n" @@ -345,11 +317,8 @@ TEST_CASE("LoadFromRummyStream: print statement outside a block", "[Rummy]") { "\n" "y = x + 1\n"); - std::string dummy_cout = - captureStdout([&]() { parthenon::LoadParameterFromRummy(in, ss2, false); }); REQUIRE_FALSE(in.DoesParameterExist("/", "print")); REQUIRE(in.GetReal("block", "y") == Approx(43.0)); - REQUIRE(dummy_cout.substr(0, 2) == "42"); } } } From 9aefb03a1694fde479810d793187e74efff02295 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 17 May 2026 07:33:34 -0600 Subject: [PATCH 45/46] Actually transfer the parameters before reading them --- tst/unit/test_rummy.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tst/unit/test_rummy.cpp b/tst/unit/test_rummy.cpp index 95c7a18a31d92..60bc19603bb66 100644 --- a/tst/unit/test_rummy.cpp +++ b/tst/unit/test_rummy.cpp @@ -11,7 +11,6 @@ // the public, perform publicly and display publicly, and to permit others to do so. //======================================================================================== -#include #include #include #include @@ -316,7 +315,7 @@ TEST_CASE("LoadFromRummyStream: print statement outside a block", "[Rummy]") { "print(x)\n" "\n" "y = x + 1\n"); - + parthenon::LoadParameterFromRummy(in, ss2, true); REQUIRE_FALSE(in.DoesParameterExist("/", "print")); REQUIRE(in.GetReal("block", "y") == Approx(43.0)); } From f08690b4425ba74c73e8d004f6073578da6f5423 Mon Sep 17 00:00:00 2001 From: Adam Dempsey Date: Tue, 28 Jul 2026 07:22:52 -0600 Subject: [PATCH 46/46] Don't rely on % to auto detect rummy decks --- src/parameter_parsers/rummy_parser.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/parameter_parsers/rummy_parser.cpp b/src/parameter_parsers/rummy_parser.cpp index 16200a628559e..93c2f08e0c648 100644 --- a/src/parameter_parsers/rummy_parser.cpp +++ b/src/parameter_parsers/rummy_parser.cpp @@ -226,7 +226,8 @@ bool IsRummyFormat(std::istream &is, const bool command_line) { std::string value_part = SanitizeString(line.substr(eq_pos + 1)); // do not include +- because they can be used in exponential notation. // / can be used in command line arguments - if (value_part.find_first_of("*\"[%^|") != std::string::npos) { + // % can be used in data format + if (value_part.find_first_of("*\"[^|") != std::string::npos) { return restore_and_return(true); } }