SpECTRE  v2024.04.12
Option Parsing

SpECTRE can read YAML configuration files at runtime to set parameters and choose between classes implementing an interface. Options are parsed during code initialization and can be used to construct objects placed in the Parallel::GlobalCache and options passed to the parallel components. The types necessary to mark objects for parsing are declared in Options/Options.hpp.

Metadata and options

YAML input files begin with a metadata section, terminated by ---, and followed by the executable options:

# Metadata here
Description: |
Briefly describe the configuration and link to papers for details.
---
# Options start here

The metadata section may also be empty:

---
---
# Options start here

You only need the leading --- marker if the metadata section is empty. This is YAML's "document start marker" (see the YAML spec). Any metadata fields at the beginning of the file also imply the start of a document, so you don't need the first --- marker.

Metadata provide information for tools, whereas options provide information to the executable. See tools like CheckOutputFiles for details on the metadata fields that they use. Metadata can also provide information on how to run the input file, such as the name and version of the executable, and a description that may refer to published papers for details on the configuration. Options are defined by the executable and detailed below.

General option format

An option is defined by an "option tag", represented by a struct. At minimum, the struct must declare the type of the object to be parsed and provide a brief description of the meaning. The name of the option in the input file defaults to the name of the struct (excluding any template parameters and scope information), but can be overridden by providing a static name() function. Several other pieces of information, such as suggestions, limits and grouping, may be provided if desired. This information is all included in the generated help output.

If an option has a suggested value, the value is specified in the input file as usual, but a warning will be issued if the specified value does not match the suggestion.

Examples:

struct Bounded {
using type = int;
static constexpr Options::String help = {
"Option with bounds and a suggested value"};
// These are optional
static type suggested_value() { return 3; }
static type lower_bound() { return 2; }
static type upper_bound() { return 10; }
};
T lower_bound(T... args)
const char *const String
The string used in option structs.
Definition: String.hpp:8
T upper_bound(T... args)
struct VectorOption {
using type = std::vector<int>;
static constexpr Options::String help = {"A vector with length limits"};
// These are optional
static std::string name() {
return "Vector"; // defaults to "VectorOption"
}
static size_t lower_bound_on_size() { return 2; }
static size_t upper_bound_on_size() { return 5; }
};
std::string name()
Return the result of the name() member of a class. If a class doesn't have a name() member,...
Definition: PrettyType.hpp:733

The option type can be any type understood natively by yaml-cpp (fundamentals, std::string, and std::map, std::vector, std::list, std::array, and std::pair of parsable types) and types SpECTRE adds support for. SpECTRE adds std::unordered_map (but only with ordered keys), std::variant (with alternatives tested in order), and various classes marked as constructible in their declarations.

An option tag can be placed in a group by adding a group type alias to the struct. The alias should refer to a type that, like option tags, defines a help string and may override a static name() function.

Example:

struct Group {
static constexpr Options::String help = {"Group halp"};
};
struct GroupedTag {
using type = int;
static constexpr Options::String help = {"Tag halp"};
using group = Group;
};

Constructible classes

A class that defines static constexpr Options::String help and a typelist of option structs options can be created by the option parser. When the class is requested, the option parser will parse each of the options in the options list, and then supply them to the constructor of the class. A class can use Options::Alternatives to support more than one possible set of options for its creation. (See Custom parsing below for more general creation mechanisms.)

Unlike option descriptions, which should be brief, the class help string has no length limits and should give a description of the class and any necessary discussion of its options beyond what can be described in their individual help strings.

Creatable classes must be default constructible and move assignable.

The Options::Context is an optional argument to the constructor that should be used when the constructor checks for validity of the input. If the input is invalid, PARSE_ERROR is used to propagate the error message back through the options ensuring that the error message will have a full backtrace so it is easy for the user to diagnose.

Example:

class CreateFromOptions {
public:
struct CfoOption {
using type = std::string;
static constexpr Options::String help = {"Option help text"};
};
static constexpr Options::String help = {"Class help text"};
using options = tmpl::list<CfoOption>;
CreateFromOptions() = default;
// The Options::Context argument can be left off if unneeded.
explicit CreateFromOptions(std::string str,
const Options::Context& context = {})
: str_(std::move(str)) {
if (str_[0] != 'f') {
PARSE_ERROR(context,
"Option must start with an 'f' but is '" << str_ << "'");
}
}
std::string str_{};
};
struct Cfo {
using type = CreateFromOptions;
static constexpr Options::String help = {"help"};
};
const char* const input_file_text = R"(
Cfo:
CfoOption: foo
)";
#define PARSE_ERROR(context, m)
Like ERROR("\n" << (context) << m), but instead throws an exception that will be caught in a higher l...
Definition: ParseError.hpp:41
Information about the nested operations being performed by the parser, for use in printing errors....
Definition: Context.hpp:17

Classes may use the Metavariables struct, which is effectively the compile time input file, in their parsing by templating the options type alias or by taking the Metavariables as a final argument to the constructor (after the Options::Context).

Example:

class CreateFromOptionsWithMetavariables {
public:
template <typename Metavariables>
struct CfoOption {
static std::string name() { return Metavariables::option_name(); }
using type = std::string;
static constexpr Options::String help = {"Option help text"};
};
static constexpr Options::String help = {"Class help text"};
template <typename Metavariables>
using options = tmpl::list<CfoOption<Metavariables>>;
CreateFromOptionsWithMetavariables() = default;
template <typename Metavariables>
CreateFromOptionsWithMetavariables(std::string str,
const Options::Context& /*context*/,
Metavariables /*meta*/)
: str_(std::move(str)),
expected_(str_ == Metavariables::expected_string()) {}
std::string str_{};
bool expected_{false};
};
struct Metavariables {
static std::string option_name() { return "MetaName"; }
static std::string expected_string() { return "MetaString"; }
};
struct CfoWithMetavariables {
using type = CreateFromOptionsWithMetavariables;
static constexpr Options::String help = {"help"};
};
const char* const input_file_text_with_metavariables = R"(
CfoWithMetavariables:
MetaName: MetaString
)";

Factory

The factory interface creates an object of type std::unique_ptr<Base> containing a pointer to some class derived from Base. The list of creatable derived classes is specified in the factory_creation struct in the metavariables, which must contain a factory_classes type alias that is a tmpl::map from base classes to lists of derived classes:

struct factory_creation
: tt::ConformsTo<Options::protocols::FactoryCreation> {
using factory_classes = tmpl::map<
tmpl::pair<OptionTest, tmpl::list<Test1, Test2, TestWithArg,
TestWithArg2, TestWithMetavars>>,
tmpl::pair<OtherBase, tmpl::list<OtherDerived>>>;
};
Indicate a class conforms to the Protocol.
Definition: ProtocolHelpers.hpp:22

When a std::unique_ptr<Base> is requested, the factory will expect a single YAML argument specifying the name of the class (as given by a static name() function or, lacking that, the actual class name). If the derived class takes no arguments, the name can be given as a YAML string, otherwise it must be given as a single key-value pair, with the key the name of the class. The value portion of this pair is then used to create the requested derived class in the same way as an explicitly constructible class. Examples:

OptionType: Test2
OptionType:
TestWithArg:
Arg: stuff

Custom parsing

Occasionally, the requirements imposed by the default creation mechanism are too stringent. In these cases, the construction algorithm can be overridden by providing a specialization of the struct

template <typename T>
template <typename Metavariables>
static T create(const Options::Option& options);
};
The type that options are passed around as. Contains YAML node data and an Context.
Definition: Options.hpp:35
Used by the parser to create an object. The default action is to parse options using T::options....
Definition: Options.hpp:75

The create function can perform any operations required to construct the object.

Example of using a specialization to parse an enum:

namespace {
enum class CreateFromOptionsAnimal { Cat, Dog };
struct CfoAnimal {
using type = CreateFromOptionsAnimal;
static constexpr Options::String help = {"Option help text"};
};
} // namespace
template <>
struct Options::create_from_yaml<CreateFromOptionsAnimal> {
template <typename Metavariables>
static CreateFromOptionsAnimal create(const Options::Option& options) {
const auto animal = options.parse_as<std::string>();
if (animal == "Cat") {
return CreateFromOptionsAnimal::Cat;
}
if (animal == "Dog") {
return CreateFromOptionsAnimal::Dog;
}
PARSE_ERROR(options.context(),
"CreateFromOptionsAnimal must be 'Cat' or 'Dog'");
}
};
T parse_as() const
Convert to an object of type T.
Definition: ParseOptions.hpp:87

Note that in the case where the create function does not need to use the Metavariables it is recommended that a general implementation forward to an explicit instantiation with void as the Metavariables type. The reason for using void specialization is to reduce compile time. Since we only need one full implementation of the function independent of what type Metavariables is, we should only parse and compile it once. By having a specialization on void (or some other non-metavariables type like NoSuchType) we can handle the metavariables-independent case efficiently. As a concrete example, the general definition and forward declaration of the void specialization in the header file would be:

namespace {
enum class CreateFromOptionsExoticAnimal { MexicanWalkingFish, Platypus };
struct CfoExoticAnimal {
using type = CreateFromOptionsExoticAnimal;
static constexpr Options::String help = {"Option help text"};
};
} // namespace
template <>
struct Options::create_from_yaml<CreateFromOptionsExoticAnimal> {
template <typename Metavariables>
static CreateFromOptionsExoticAnimal create(const Options::Option& options) {
return create<void>(options);
}
};
template <>
CreateFromOptionsExoticAnimal
const Options::Option& options);

while in the cpp file the definition of the void specialization is:

template <>
CreateFromOptionsExoticAnimal
const Options::Option& options) {
const auto animal = options.parse_as<std::string>();
if (animal == "MexicanWalkingFish") {
return CreateFromOptionsExoticAnimal::MexicanWalkingFish;
}
if (animal == "Platypus") {
return CreateFromOptionsExoticAnimal::Platypus;
}
PARSE_ERROR(options.context(),
"CreateFromOptionsExoticAnimal must be 'MexicanWalkingFish' or "
"'Platypus'");
}