Skip to content

Commit

Permalink
InputSpec: new selection for common use case
Browse files Browse the repository at this point in the history
We frequently want to select different parameters based on the value
of a parameter. This is now easier with the new selection overload.
  • Loading branch information
sebproell committed Feb 28, 2025
1 parent 572fd00 commit 0cfec07
Show file tree
Hide file tree
Showing 2 changed files with 167 additions and 0 deletions.
98 changes: 98 additions & 0 deletions src/core/io/src/4C_io_input_spec_builders.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,19 @@ namespace Core::IO
[&](const auto& val) { return this->operator()(val.second, size_info + 1); });
}
};


//! Helper for selection().
struct BasedOn
{
std::string name;
};

//! Helper for selection().
struct FromChoices
{
std::map<std::string, InputSpec> choices;
};
} // namespace Internal

/**
Expand Down Expand Up @@ -1033,6 +1046,48 @@ namespace Core::IO
[[nodiscard]] InputSpec group(
std::string name, std::vector<InputSpec> specs, GroupData data = {});

/**
* This function is used to select one of multiple InputSpecs based on the value of a parameter
* with name given in @p selector. The selector parameter is always put inside a group with the
* given @p name. For every possible value of the selector, a different InputSpec is expected
* inside the group @p name. For example:
*
* @code
* auto spec = selection<std::string>("Time integration", based_on("scheme"),
* from_choices({
* {"OST", parameter<double>("theta")},
* {"GenAlpha", all_of({
* parameter<double>("alpha_f"),
* parameter<double>("alpha_m"),
* }),
* }));
* @endcode
*
* will match the following input:
*
* @code
* Time integration:
* scheme: OST
* theta: 0.5
* @endcode
*
* or the following input:
*
* @code
* Time integration:
* scheme: GenAlpha
* alpha_f: 1
* alpha_m: 0.5
* @endcode
*
* @note This function uses the fluent interface idiom and wraps the name of the @p selector
* with based_on() and the choices with from_choices().
*/
template <typename T>
requires(std::is_same_v<T, std::string>)
[[nodiscard]] InputSpec selection(std::string name, Internal::BasedOn selector,
Internal::FromChoices choices, GroupData data = {});

/**
* All of the given InputSpecs are expected, e.g.,
*
Expand Down Expand Up @@ -1190,6 +1245,17 @@ namespace Core::IO
* InputSpec, you should rarely need the list() function.
*/
[[nodiscard]] InputSpec list(std::string name, InputSpec spec, ListData data = {});

/**
* A fluent interface helper. Used for selection().
*/
[[nodiscard]] inline Internal::BasedOn based_on(std::string name);

/**
* A fluent interface helper. Used for selection().
*/
[[nodiscard]] inline Internal::FromChoices from_choices(
std::map<std::string, InputSpec> choices);
} // namespace InputSpecBuilders
} // namespace Core::IO

Expand Down Expand Up @@ -1655,6 +1721,24 @@ Core::IO::InputSpec Core::IO::InputSpecBuilders::parameter(
});
}

template <typename T>
requires(std::is_same_v<T, std::string>)
Core::IO::InputSpec Core::IO::InputSpecBuilders::selection(
std::string name, Internal::BasedOn selector, Internal::FromChoices choices, GroupData data)
{
std::vector<InputSpec> specs;
specs.reserve(choices.choices.size());
for (auto&& [choice_name, choice_spec] : choices.choices)
{
specs.emplace_back(all_of({
selection<std::string>(selector.name, {choice_name},
{.description = "Selects which other parameters are allowed in this group."}),
std::move(choice_spec),
}));
}
return group(name, {one_of(specs)}, data);
}


template <typename T>
Core::IO::InputSpec Core::IO::InputSpecBuilders::Internal::selection_internal(
Expand Down Expand Up @@ -1766,6 +1850,20 @@ auto Core::IO::InputSpecBuilders::store_index_as(std::string name, std::vector<T
};
}


Core::IO::InputSpecBuilders::Internal::BasedOn Core::IO::InputSpecBuilders::based_on(
std::string name)
{
return Internal::BasedOn{name};
}

Core::IO::InputSpecBuilders::Internal::FromChoices Core::IO::InputSpecBuilders::from_choices(
std::map<std::string, InputSpec> choices)
{
return Internal::FromChoices{std::move(choices)};
}


FOUR_C_NAMESPACE_CLOSE

#endif
69 changes: 69 additions & 0 deletions src/core/io/tests/4C_io_input_spec_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,75 @@ required: true
}


TEST(InputSpecTest, MatchYamlGroupWithSelection)
{
auto spec = selection<std::string>("model", based_on("type"),
from_choices({
{"linear", parameter<double>("coefficient")},
{"quadratic", one_of({
all_of({
parameter<int>("a"),
parameter<double>("b"),
}),
parameter<double>("c"),
})},
}));

{
SCOPED_TRACE("First selection");
ryml::Tree tree = init_yaml_tree_with_exceptions();
ryml::NodeRef root = tree.rootref();
ryml::parse_in_arena(R"(model:
type: linear
coefficient: 1.0
)",
root);

ConstYamlNodeRef node(root, "");
InputParameterContainer container;
spec.match(node, container);
EXPECT_EQ(container.group("model").get<std::string>("type"), "linear");
EXPECT_EQ(container.group("model").get<double>("coefficient"), 1.0);
}

{
SCOPED_TRACE("Second selection");
ryml::Tree tree = init_yaml_tree_with_exceptions();
ryml::NodeRef root = tree.rootref();
ryml::parse_in_arena(R"(model:
type: quadratic
a: 1
b: 2.0
)",
root);

ConstYamlNodeRef node(root, "");
InputParameterContainer container;
spec.match(node, container);
EXPECT_EQ(container.group("model").get<std::string>("type"), "quadratic");
EXPECT_EQ(container.group("model").get<int>("a"), 1);
EXPECT_EQ(container.group("model").get<double>("b"), 2.0);
}

{
SCOPED_TRACE("Second selection, other one_of");
ryml::Tree tree = init_yaml_tree_with_exceptions();
ryml::NodeRef root = tree.rootref();
ryml::parse_in_arena(R"(model:
type: quadratic
c: 3.0
)",
root);

ConstYamlNodeRef node(root, "");
InputParameterContainer container;
spec.match(node, container);
EXPECT_EQ(container.group("model").get<std::string>("type"), "quadratic");
EXPECT_EQ(container.group("model").get<double>("c"), 3.0);
}
}


TEST(InputSpecTest, MatchYamlAllOf)
{
auto spec = all_of({
Expand Down

0 comments on commit 0cfec07

Please sign in to comment.