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::ConstGlobalCache and options passed to the parallel components. The types necessary to mark objects for parsing are declared in Options/Options.hpp.

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 defaults, limits and grouping, may be provided if desired. This information is all included in the generated help output.

Examples:

struct Bounded {
using type = int;
static constexpr OptionString help = {
"Option with bounds and a default value"};
// These are optional
static type default_value() noexcept { return 3; }
static type lower_bound() noexcept { return 2; }
static type upper_bound() noexcept { return 10; }
};
struct VectorOption {
using type = std::vector<int>;
static constexpr OptionString help = {"A vector with length limits"};
// These are optional
static std::string name() noexcept {
return "Vector"; // defaults to "VectorOption"
}
static size_t lower_bound_on_size() { return 2; }
static size_t upper_bound_on_size() { return 5; }
};

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), 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 OptionString help = {"Group halp"};
};
struct GroupedTag {
using type = int;
static constexpr OptionString help = {"Tag halp"};
using group = Group;
};

Constructible classes

A class that defines static constexpr OptionString 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. (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 OptionContext 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. Finally, after the OptionContext the constructor may optionally take the Metavariables struct, which is effectively the compile time input file, and the constructor can use the Metavariables for whatever it wants, including additional option parsing.

Example:

template <bool Valid>
struct Metavariables {
static constexpr bool valid = Valid;
};
template <typename>
class CreateFromOptions;
struct CFO {
using type = CreateFromOptions<int>;
static constexpr OptionString help = {"help"};
};
template <typename T>
class CreateFromOptions {
public:
struct Option {
using type = std::string;
static constexpr OptionString help = {"Option help text"};
};
using options = tmpl::list<Option>;
static constexpr OptionString help = {"Class help text"};
CreateFromOptions() = default;
// The Metavariables arguments can be left off if unneeded, and the
// OptionContext as well.
template <typename Metavariables>
CreateFromOptions(std::string str, const OptionContext& context,
Metavariables /*meta*/)
: str_(std::move(str)), valid_(Metavariables::valid) {
if (str_[0] != 'f') {
PARSE_ERROR(context,
"Option must start with an 'f' but is '" << str_ << "'");
}
}
std::string str_{};
bool valid_{false};
};
const char* const input_file_text = R"(
CFO:
Option: foo
)";

Factory

The factory interface creates an object of type std::unique_ptr<Base> containing a pointer to some class derived from Base. The base class must define a type alias listing the derived classes that can be created.

using creatable_classes = tmpl::list<Derived1, ...>;

The requested derived class is created in the same way as an explicitly constructible class.

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 Option& options);
};

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 OptionString help = {"Option help text"};
};
} // namespace
template <>
struct create_from_yaml<CreateFromOptionsAnimal> {
template <typename Metavariables>
static CreateFromOptionsAnimal create(const Option& options) {
const std::string 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'");
}
};

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 OptionString help = {"Option help text"};
};
} // namespace
template <>
struct create_from_yaml<CreateFromOptionsExoticAnimal> {
template <typename Metavariables>
static CreateFromOptionsExoticAnimal create(const Option& options) {
return create<void>(options);
}
};
template <>
CreateFromOptionsExoticAnimal
const Option& options);

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

template <>
CreateFromOptionsExoticAnimal
const Option& options) {
const std::string 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'");
}