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 9f2f92eb290aa..51fc13a9eaa9a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -495,6 +495,17 @@ 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 + 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) +endif() + + if (PARTHENON_ENABLE_FFT) if(NOT ENABLE_MPI) message(FATAL_ERROR 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..0abfd43f6b0b5 --- /dev/null +++ b/doc/sphinx/src/rummy_input.rst @@ -0,0 +1,446 @@ +.. _rummy_input: + +Rummy Input Files +================= + +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 +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 +---------------- + +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 + + 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) + + +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 +---------------- + +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 native (Rummy) parser, place ``# use native`` (``# use rummy``) at the 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 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); } } diff --git a/external/rummy b/external/rummy new file mode 160000 index 0000000000000..6734a06292b70 --- /dev/null +++ b/external/rummy @@ -0,0 +1 @@ +Subproject commit 6734a06292b70fc2cbb756e66a073fc7c060013c diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 93ce46a8c1962..8861b98776e2d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -248,6 +248,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 @@ -421,6 +424,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) @@ -446,6 +451,19 @@ target_include_directories(parthenon PUBLIC install(TARGETS parthenon EXPORT parthenonTargets) + +if(NOT Rummy_FOUND) + 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 + 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") diff --git a/src/argument_parser.hpp b/src/argument_parser.hpp index 134a48ea7fa84..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" @@ -51,7 +52,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 +129,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 +143,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 256d91a4cd9bc..39bb3fd03d8be 100644 --- a/src/parameter_input.cpp +++ b/src/parameter_input.cpp @@ -72,12 +72,28 @@ 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_{} {} ParameterInput::ParameterInput(std::string input_filename) : last_filename_{} { + ReadFile(input_filename); +} + +void ParameterInput::ReadFile(const std::string &input_filename) { IOWrapper infile; infile.Open(input_filename.c_str(), IOWrapper::FileMode::read); LoadFromFile(infile); @@ -329,15 +345,21 @@ 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."); - std::string input_text, block, name, value; - std::stringstream msg; - for (int i = 1; i < argc; i++) { - input_text = argv[i]; + if (mods.empty()) return; + std::stringstream ss; + for (const auto &mod : mods) { + ss << mod << " # From command line\n"; + } + + // Native parsing + 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 "=" @@ -345,6 +367,7 @@ void ParameterInput::ModifyFromCmdline(int argc, char *argv[]) { if ((slash_posn == std::string::npos) || (equal_posn == std::string::npos)) continue; if (slash_posn > equal_posn) { + std::stringstream msg; msg << "'/' used as value (rhs of =) when modifying " << input_text << "." << " Please update value of change " << "logic in ModifyFromCmdline function."; @@ -352,14 +375,15 @@ void ParameterInput::ModifyFromCmdline(int argc, char *argv[]) { } // 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) { + 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."; @@ -367,6 +391,7 @@ void ParameterInput::ModifyFromCmdline(int argc, char *argv[]) { } } else if (FindParameter_(block, name) == nullptr) { if (Globals::my_rank == 0) { + 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 " @@ -993,6 +1018,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 17e1d7b42f60a..a1c7d4e3ef91f 100644 --- a/src/parameter_input.hpp +++ b/src/parameter_input.hpp @@ -50,6 +50,8 @@ namespace parthenon { +std::string SanitizeString(const std::string &input); + //---------------------------------------------------------------------------------------- // Supported parameter types - single source of truth //---------------------------------------------------------------------------------------- @@ -235,11 +237,12 @@ class ParameterInput { ParameterInput(); explicit ParameterInput(std::string input_filename); ~ParameterInput(); + void ReadFile(const std::string &input_filename); // === PARSING INTERFACE === void LoadFromStream(std::istream &is); void LoadFromFile(IOWrapper &input); - 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 @@ -462,6 +465,8 @@ class ParameterInput { return ret; } + const std::vector &GetBlocks() const { return param_storage_; } + private: // === PARAMETER STORAGE (vector-of-vectors, preserves insertion order) === std::vector param_storage_; // Ordered storage (for iteration) diff --git a/src/parameter_parsers/rummy_parser.cpp b/src/parameter_parsers/rummy_parser.cpp new file mode 100644 index 0000000000000..93c2f08e0c648 --- /dev/null +++ b/src/parameter_parsers/rummy_parser.cpp @@ -0,0 +1,326 @@ +//======================================================================================== +// (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 +#include +#include + +#include + +#include "parameter_input.hpp" +#include "rummy_parser.hpp" + +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 + // % can be used in data format + 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 diff --git a/src/parameter_parsers/rummy_parser.hpp b/src/parameter_parsers/rummy_parser.hpp new file mode 100644 index 0000000000000..890b3f654a35d --- /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_ diff --git a/src/parthenon_manager.cpp b/src/parthenon_manager.cpp index 1df620c177856..f77fc8b3d2d0b 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" #ifdef PARTHENON_ENABLE_OPENPMD #include "outputs/restart_opmd.hpp" #endif @@ -137,23 +138,37 @@ ParthenonStatus ParthenonManager::ParthenonInitEnv(int argc, char *argv[]) { std::istringstream is(inputString); pinput->LoadFromStream(is); } - // If an input file was provided - 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(); - - // Populate new object for fresh simulation - } else { - pinput = std::make_unique(arg.input_filename); + // 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(argc, argv); + // 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/regression/CMakeLists.txt b/tst/regression/CMakeLists.txt index b03fd69c65d86..454b808d3d088 100644 --- a/tst/regression/CMakeLists.txt +++ b/tst/regression/CMakeLists.txt @@ -162,6 +162,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_rummy/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..0d863bde54bed --- /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 * refine_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..488ada3420a9f --- /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 diff --git a/tst/unit/CMakeLists.txt b/tst/unit/CMakeLists.txt index af6ab9c70e8aa..3f25ed8707946 100644 --- a/tst/unit/CMakeLists.txt +++ b/tst/unit/CMakeLists.txt @@ -41,6 +41,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..60bc19603bb66 --- /dev/null +++ b/tst/unit/test_rummy.cpp @@ -0,0 +1,378 @@ +//======================================================================================== +// (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. +//======================================================================================== + +#include +#include +#include +#include + +#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; + std::istringstream ss("\n" + "nx = 64\n" + "cfl = 0.4\n" + "active = true\n" + "label = \"hydro\"\n"); + parthenon::LoadParameterFromRummy(in, ss, false); + + 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"); + parthenon::LoadParameterFromRummy(in, ss, false); + + 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"); + parthenon::LoadParameterFromRummy(in, ss, false); + + 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"); + parthenon::LoadParameterFromRummy(in, ss, false); + + 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"); + parthenon::LoadParameterFromRummy(in, ss, false); + + 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::istringstream ss("\n" + "nx1 = 64\n" + "nx2 = 32\n"); + THEN("IsRummyFormat returns false") { + REQUIRE(parthenon::IsRummyFormat(ss, false) == false); + } + } + + GIVEN("A Rummy-format file: global variable before first block") { + std::istringstream ss("Lx = 1.0\n" + "\n" + "nx = 64\n"); + THEN("IsRummyFormat returns true") { + REQUIRE(parthenon::IsRummyFormat(ss, false) == true); + } + } + + GIVEN("A Rummy-format file: relative suit path <../") { + std::istringstream ss("\n" + "hydro = true\n" + "<../eos>\n" + "gamma = 1.4\n"); + THEN("IsRummyFormat returns true") { + REQUIRE(parthenon::IsRummyFormat(ss, false) == true); + } + } + + GIVEN("A Rummy-format file: ** power operator in a value") { + std::istringstream ss("\n" + "val = 2**10\n"); + THEN("IsRummyFormat returns true") { + REQUIRE(parthenon::IsRummyFormat(ss, false) == true); + } + } + + GIVEN("A Rummy-format file: first line is '# use rummy'") { + std::istringstream ss("# Use Rummy\n" + "\n" + "nx = 64\n"); + THEN("IsRummyFormat returns true") { + REQUIRE(parthenon::IsRummyFormat(ss, false) == true); + } + } + + GIVEN("A Rummy-format file: quoted string value") { + std::istringstream ss("\n" + "label = \"hydro\"\n"); + THEN("IsRummyFormat returns true") { + REQUIRE(parthenon::IsRummyFormat(ss, false) == true); + } + } + + GIVEN("A Rummy-format file: bracket vector syntax in a value") { + std::istringstream ss("\n" + "nx = [64, 32, 16]\n"); + THEN("IsRummyFormat returns true") { + REQUIRE(parthenon::IsRummyFormat(ss, false) == true); + } + } + + GIVEN("A Rummy-format file: bracket slice syntax on the LHS") { + std::istringstream ss("\n" + "nx[:2] = [64, 32]\n"); + THEN("IsRummyFormat returns true") { + REQUIRE(parthenon::IsRummyFormat(ss, false) == true); + } + } +} + +TEST_CASE("LoadFromRummyStream: ModifyFromCmdline overrides Rummy params", "[Rummy]") { + GIVEN("A Rummy stream with a parameter") { + ParameterInput in; + std::istringstream ss("\nnx = 32\n"); + parthenon::LoadParameterFromRummy(in, ss, false); + + WHEN("ModifyFromCmdline overrides the parameter") { + std::istringstream ss2("mesh.nx = 128\n"); + parthenon::LoadParameterFromRummy(in, ss2, true); + 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"); + parthenon::LoadParameterFromRummy(in, ss, false); + + 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"); + parthenon::LoadParameterFromRummy(in, ss, false); + + 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"); + LoadParameterFromRummy(in, ss, false); + + 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"); + parthenon::LoadParameterFromRummy(in, ss, false); + + 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(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"); + parthenon::LoadParameterFromRummy(in, ss2, true); + 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"); + parthenon::LoadParameterFromRummy(in, ss, false); + + 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"); + 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"); + parthenon::LoadParameterFromRummy(in, ss2, true); + + 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)); + } + } + } +}