diff --git a/include/bbp/sonata/common.h b/include/bbp/sonata/common.h index 61ce7b11..e7abd4c2 100644 --- a/include/bbp/sonata/common.h +++ b/include/bbp/sonata/common.h @@ -45,5 +45,8 @@ class SONATA_API SonataError: public std::runtime_error public: explicit SonataError(const std::string& what); }; + +#define THROW_IF_REACHED throw SonataError("Should never be reached"); + } // namespace sonata } // namespace bbp diff --git a/include/bbp/sonata/node_sets.h b/include/bbp/sonata/node_sets.h index fbd58180..58feeaab 100644 --- a/include/bbp/sonata/node_sets.h +++ b/include/bbp/sonata/node_sets.h @@ -26,9 +26,9 @@ class SONATA_API NodeSets * \throw if content cannot be parsed */ NodeSets(const std::string& content); - NodeSets(NodeSets&&); + NodeSets(NodeSets&&) noexcept; NodeSets(const NodeSets& other) = delete; - NodeSets& operator=(NodeSets&&); + NodeSets& operator=(NodeSets&&) noexcept; ~NodeSets(); /** Open a SONATA `node sets` file from a path */ diff --git a/include/bbp/sonata/nodes.h b/include/bbp/sonata/nodes.h index 16b4776e..a1e7e484 100644 --- a/include/bbp/sonata/nodes.h +++ b/include/bbp/sonata/nodes.h @@ -45,7 +45,7 @@ class SONATA_API NodePopulation: public Population * Note: This does not match dynamics_params datasets */ template - Selection matchAttributeValues(const std::string& attribute, const T value) const; + Selection matchAttributeValues(const std::string& attribute, const T values) const; /** * Like matchAttributeValues, but for vectors of values to match @@ -53,6 +53,12 @@ class SONATA_API NodePopulation: public Population template Selection matchAttributeValues(const std::string& attribute, const std::vector& values) const; + + + /** + * For named attribute, return a selection where the passed regular expression matches + */ + Selection regexMatch(const std::string& attribute, const std::string& re) const; }; //-------------------------------------------------------------------------------------------------- diff --git a/include/bbp/sonata/population.h b/include/bbp/sonata/population.h index a5a735e2..db8f4fad 100644 --- a/include/bbp/sonata/population.h +++ b/include/bbp/sonata/population.h @@ -12,6 +12,7 @@ #include "common.h" #include +#include #include // std::shared_ptr, std::unique_ptr #include #include @@ -232,6 +233,9 @@ class SONATA_API Population */ std::string _dynamicsAttributeDataType(const std::string& name) const; + template + Selection filterAttribute(const std::string& name, std::function pred) const; + protected: Population(const std::string& h5FilePath, const std::string& csvFilePath, diff --git a/python/generated/docstrings.h b/python/generated/docstrings.h index 334de8a1..d55d942a 100644 --- a/python/generated/docstrings.h +++ b/python/generated/docstrings.h @@ -75,6 +75,10 @@ Note: This does not match dynamics_params datasets)doc"; static const char *__doc_bbp_sonata_NodePopulation_matchAttributeValues_2 = R"doc(Like matchAttributeValues, but for vectors of values to match)doc"; +static const char *__doc_bbp_sonata_NodePopulation_regexMatch = +R"doc(For named attribute, return a selection where the passed regular +expression matches)doc"; + static const char *__doc_bbp_sonata_NodeSets = R"doc()doc"; static const char *__doc_bbp_sonata_NodeSets_NodeSets = @@ -182,6 +186,8 @@ Parameter ``name``: Throws: if there is no such attribute for the population)doc"; +static const char *__doc_bbp_sonata_Population_filterAttribute = R"doc()doc"; + static const char *__doc_bbp_sonata_Population_getAttribute = R"doc(Get attribute values for given {element} Selection diff --git a/python/tests/test.py b/python/tests/test.py index 48c09bc5..7d4d6c5c 100644 --- a/python/tests/test.py +++ b/python/tests/test.py @@ -462,6 +462,15 @@ def test_NodeSet_toJSON(self): "model_type": "point", "node_id": [1, 2, 3, 5, 7, 9] }, + "power_number_test": { + "numeric_attribute_gt": { "$gt": 3 }, + "numeric_attribute_lt": { "$lt": 3 }, + "numeric_attribute_gte": { "$gte": 3 }, + "numeric_attribute_lte": { "$lte": 3 } + }, + "power_regex_test": { + "string_attr": { "$regex": "^[s][o]me value$" } + }, "combined": ["bio_layer45", "V1_point_prime"] }''' new = NodeSets(j).toJSON() diff --git a/src/node_sets.cpp b/src/node_sets.cpp index 98fc2c5b..434e3687 100644 --- a/src/node_sets.cpp +++ b/src/node_sets.cpp @@ -36,7 +36,7 @@ class NodeSets; class NodeSetRule { public: - virtual ~NodeSetRule(){}; + virtual ~NodeSetRule() = default; virtual Selection materialize(const NodeSets&, const NodePopulation&) const = 0; virtual std::string toJSON() const = 0; @@ -51,7 +51,7 @@ class NodeSets std::map node_sets_; public: - NodeSets(const std::string& content) { + explicit NodeSets(const std::string& content) { json j = json::parse(content); if (!j.is_object()) { throw SonataError("Top level node_set must be an object"); @@ -176,13 +176,134 @@ class NodeSetBasicNodeIds: public NodeSetRule Selection::Values values_; }; +class NodeSetBasicOperatorString: public NodeSetRule +{ + public: + explicit NodeSetBasicOperatorString(const std::string& attribute, + const std::string& op, + const std::string& value) + : op_(string2op(op)) + , attribute_(attribute) + , value_(value) {} + + Selection materialize(const detail::NodeSets& /* unused */, + const NodePopulation& np) const final { + switch (op_) { + case Op::regex: + return np.regexMatch(attribute_, value_); + default: // LCOV_EXCL_LINE + THROW_IF_REACHED // LCOV_EXCL_LINE + } + } + + std::string toJSON() const final { + return fmt::format(R"("{}": {{ "{}": "{}" }})", attribute_, op2string(op_), value_); + } + + enum class Op { + regex = 1, + }; + + static Op string2op(const std::string& s) { + if (s == "$regex") { + return Op::regex; + } + throw SonataError(fmt::format("Operator '{}' not available for strings", s)); + } + + static std::string op2string(const Op op) { + switch (op) { + case Op::regex: + return "$regex"; + default: // LCOV_EXCL_LINE + THROW_IF_REACHED // LCOV_EXCL_LINE + } + } + + private: + Op op_; + std::string attribute_; + std::string value_; +}; + +class NodeSetBasicOperatorNumeric: public NodeSetRule +{ + public: + explicit NodeSetBasicOperatorNumeric(const std::string& name, + const std::string& op, + double value) + : name_(name) + , value_(value) + , op_(string2op(op)) {} + + Selection materialize(const detail::NodeSets& /* unused */, + const NodePopulation& np) const final { + switch (op_) { + case Op::gt: + return np.filterAttribute(name_, [=](const double v) { return v > value_; }); + case Op::lt: + return np.filterAttribute(name_, [=](const double v) { return v < value_; }); + case Op::gte: + return np.filterAttribute(name_, [=](const double v) { return v >= value_; }); + case Op::lte: + return np.filterAttribute(name_, [=](const double v) { return v <= value_; }); + default: // LCOV_EXCL_LINE + THROW_IF_REACHED // LCOV_EXCL_LINE + } + } + + std::string toJSON() const final { + return fmt::format(R"("{}": {{ "{}": {} }})", name_, op2string(op_), value_); + } + + enum class Op { + gt = 1, + lt = 2, + gte = 3, + lte = 4, + }; + + static Op string2op(const std::string& s) { + if (s == "$gt") { + return Op::gt; + } else if (s == "$lt") { + return Op::lt; + } else if (s == "$gte") { + return Op::gte; + } else if (s == "$lte") { + return Op::lte; + } + throw SonataError(fmt::format("Operator '{}' not available for numeric", s)); + } + + static std::string op2string(const Op op) { + switch (op) { + case Op::gt: + return "$gt"; + case Op::lt: + return "$lt"; + case Op::gte: + return "$gte"; + case Op::lte: + return "$lte"; + default: // LCOV_EXCL_LINE + THROW_IF_REACHED // LCOV_EXCL_LINE + } + } + + private: + std::string name_; + double value_; + Op op_; +}; + using CompoundTargets = std::vector; class NodeSetCompoundRule: public NodeSetRule { public: - NodeSetCompoundRule(std::string name, const CompoundTargets& targets) + NodeSetCompoundRule(std::string name, CompoundTargets targets) : name_(std::move(name)) - , targets_(targets) {} + , targets_(std::move(targets)) {} Selection materialize(const detail::NodeSets& ns, const NodePopulation& np) const final { Selection ret{{}}; @@ -287,6 +408,26 @@ NodeSetRules _dispatch_node(const json& contents) { } else { throw SonataError("Unknown array type"); } + } else if (el.value().is_object()) { + const auto& definition = el.value(); + if (definition.size() != 1) { + throw SonataError( + fmt::format("Operator '{}' must have object with one key value pair", + attribute)); + } + const auto& key = definition.begin().key(); + const auto& value = definition.begin().value(); + if (value.is_number()) { + ret.emplace_back( + new NodeSetBasicOperatorNumeric(attribute, key, value.get())); + } else if (value.is_string()) { + ret.emplace_back( + new NodeSetBasicOperatorString(attribute, key, value.get())); + } else { + throw SonataError("Unknown operator"); + } + } else { + THROW_IF_REACHED // LCOV_EXCL_LINE } } return ret; @@ -354,8 +495,8 @@ void parse_compound(const json& j, std::map& node_set NodeSets::NodeSets(const std::string& content) : impl_(new detail::NodeSets(content)) {} -NodeSets::NodeSets(NodeSets&&) = default; -NodeSets& NodeSets::operator=(NodeSets&&) = default; +NodeSets::NodeSets(NodeSets&&) noexcept = default; +NodeSets& NodeSets::operator=(NodeSets&&) noexcept = default; NodeSets::~NodeSets() = default; NodeSets NodeSets::fromFile(const std::string& path) { diff --git a/src/nodes.cpp b/src/nodes.cpp index f5c4d8c8..7183cea8 100644 --- a/src/nodes.cpp +++ b/src/nodes.cpp @@ -8,120 +8,140 @@ *************************************************************************/ #include "population.hpp" +#include "utils.h" -#include // std::binary_search +#include // std::binary_search, std::max_element, std::any_of +#include #include #include #include - namespace bbp { namespace sonata { -//-------------------------------------------------------------------------------------------------- - -NodePopulation::NodePopulation(const std::string& h5FilePath, - const std::string& csvFilePath, - const std::string& name) - : Population(h5FilePath, csvFilePath, name, ELEMENT) {} - - namespace { template -Selection _getMatchingSelection(const std::vector& values, const std::vector& wanted) { - Selection::Values idx; - Selection::Value id = 0; - - if (wanted.size() == 1) { - for (const auto& v : values) { - if (v == wanted[0]) { - idx.push_back(id); - } - ++id; - } +Selection _matchAttributeValues(const NodePopulation& population, + const std::string& name, + const std::vector& wanted) { + if (wanted.empty()) { + return Selection({}); + } else if (wanted.size() == 1) { + return population.filterAttribute(name, + [&wanted](const T& v) { return wanted[0] == v; }); } else { std::vector wanted_sorted(wanted); std::sort(wanted_sorted.begin(), wanted_sorted.end()); - for (const auto& v : values) { - if (std::binary_search(wanted_sorted.cbegin(), wanted_sorted.cend(), v)) { - idx.push_back(id); + + const auto pred = [&wanted_sorted](const T& v) { + return std::binary_search(wanted_sorted.cbegin(), wanted_sorted.cend(), v); + }; + return population.filterAttribute(name, pred); + } +} + +bool is_unsigned_int(const HighFive::DataType& dtype) { + return dtype == HighFive::AtomicType() || dtype == HighFive::AtomicType() || + dtype == HighFive::AtomicType() || dtype == HighFive::AtomicType(); +} + +bool is_signed_int(const HighFive::DataType& dtype) { + return dtype == HighFive::AtomicType() || dtype == HighFive::AtomicType() || + dtype == HighFive::AtomicType() || dtype == HighFive::AtomicType(); +} +bool is_floating(const HighFive::DataType& dtype) { + return dtype == HighFive::AtomicType() || dtype == HighFive::AtomicType(); +} + +template +Selection _filterStringAttribute(const NodePopulation& population, + std::string name, + UnaryPredicate pred) { + if (population.enumerationNames().count(name) > 0) { + const auto& enum_values = population.enumerationValues(name); + // it's assumed that the cardinality of a @library is low + // enough that a std::vector won't be too large + std::vector wanted_enum_mask(enum_values.size()); + + bool has_elements = false; + for (size_t i = 0; i < enum_values.size(); ++i) { + if (pred(enum_values[i])) { + wanted_enum_mask[i] = true; + has_elements = true; } - ++id; } + + if (!has_elements) { + return Selection({}); + } + + const auto& values = population.getEnumeration(name, population.selectAll()); + return _getMatchingSelection(values, [&wanted_enum_mask](const size_t v) { + return wanted_enum_mask.at(v); + }); } - return Selection::fromValues(idx); -} -template -Selection _matchAttributeValues(const NodePopulation& population, - const std::string& name, - const std::vector& wanted) { - return _getMatchingSelection(population.getAttribute(name, population.selectAll()), wanted); + // normal, non-enum, attribute + return population.filterAttribute(name, pred); } } // anonymous namespace +NodePopulation::NodePopulation(const std::string& h5FilePath, + const std::string& csvFilePath, + const std::string& name) + : Population(h5FilePath, csvFilePath, name, ELEMENT) {} + +Selection NodePopulation::regexMatch(const std::string& name, const std::string& regex) const { + std::regex re(regex); + const auto pred = [re](const std::string& v) { + std::smatch match; + std::regex_search(v, match, re); + return !match.empty(); + }; + return _filterStringAttribute(*this, name, pred); +} + template -Selection NodePopulation::matchAttributeValues(const std::string& name, - const std::vector& value) const { - auto dtype = impl_->getAttributeDataSet(name).getDataType(); - if (dtype == HighFive::AtomicType() || dtype == HighFive::AtomicType() || - dtype == HighFive::AtomicType() || dtype == HighFive::AtomicType() || - dtype == HighFive::AtomicType() || dtype == HighFive::AtomicType() || - dtype == HighFive::AtomicType() || dtype == HighFive::AtomicType()) { - return _matchAttributeValues(*this, name, value); - } else if (dtype == HighFive::AtomicType() || dtype == HighFive::AtomicType()) { +Selection NodePopulation::matchAttributeValues(const std::string& attribute, + const std::vector& values) const { + auto dtype = impl_->getAttributeDataSet(attribute).getDataType(); + if (is_unsigned_int(dtype) || is_signed_int(dtype)) { + return _matchAttributeValues(*this, attribute, values); + } else if (is_floating(dtype)) { throw SonataError("Exact comparison for float/double explicitly not supported"); } else { throw SonataError( - fmt::format("Unexpected datatype for dataset '{}'", _attributeDataType(name))); + fmt::format("Unexpected datatype for dataset '{}'", _attributeDataType(attribute))); } } template -Selection NodePopulation::matchAttributeValues(const std::string& name, const T value) const { +Selection NodePopulation::matchAttributeValues(const std::string& attribute, const T value) const { std::vector values{value}; - return matchAttributeValues(name, values); + return matchAttributeValues(attribute, values); } template <> Selection NodePopulation::matchAttributeValues( - const std::string& name, const std::vector& values) const { - if (enumerationNames().count(name) > 0) { - const auto enum_values = enumerationValues(name); - std::vector wanted_enum_value; - wanted_enum_value.reserve(values.size()); - for (const auto& v : values) { - const auto wanted_index = std::find(enum_values.cbegin(), enum_values.cend(), v); - if (wanted_index != enum_values.cend()) { - wanted_enum_value.push_back(wanted_index - enum_values.cbegin()); - } - } - - if (wanted_enum_value.empty()) { - return Selection({}); - } - - return _getMatchingSelection(getEnumeration(name, selectAll()), - wanted_enum_value); - } + const std::string& attribute, const std::vector& values) const { + std::vector values_sorted(values); + std::sort(values_sorted.begin(), values_sorted.end()); - auto dtype = impl_->getAttributeDataSet(name).getDataType(); - if (dtype != HighFive::AtomicType()) { - throw SonataError("H5 dataset must be a string"); - } + const auto pred = [&values_sorted](const std::string& v) { + return std::binary_search(values_sorted.cbegin(), values_sorted.cend(), v); + }; - // normal, non-enum, attribute - return _matchAttributeValues(*this, name, values); + return _filterStringAttribute(*this, attribute, pred); } template <> -Selection NodePopulation::matchAttributeValues(const std::string& name, +Selection NodePopulation::matchAttributeValues(const std::string& attribute, const std::string value) const { std::vector values{value}; - return matchAttributeValues(name, values); + return matchAttributeValues(attribute, values); } diff --git a/src/population.cpp b/src/population.cpp index 0e4d6a1c..73d4adf4 100644 --- a/src/population.cpp +++ b/src/population.cpp @@ -11,11 +11,12 @@ #include // std::move #include "hdf5_mutex.hpp" -#include "population.hpp" +#include "utils.h" #include #include +#include "population.hpp" namespace bbp { namespace sonata { @@ -193,7 +194,7 @@ std::string _getDataType(const HighFive::DataSet& dset, const std::string& name) } } -} // unnamed namespace +} // anonymous namespace Population::Population(const std::string& h5FilePath, @@ -344,6 +345,26 @@ std::string Population::_dynamicsAttributeDataType(const std::string& name) cons return _getDataType(impl_->getDynamicsAttributeDataSet(name), name); } +template <> +Selection Population::filterAttribute(const std::string& name, + std::function pred) const { + auto dtype = impl_->getAttributeDataSet(name).getDataType(); + if (dtype != HighFive::AtomicType()) { + throw SonataError("H5 dataset must be a string"); + } + + const auto& values = getAttribute(name, selectAll()); + return _getMatchingSelection(values, pred); +} + +template +Selection Population::filterAttribute(const std::string& name, + std::function pred) const { + const auto& values = getAttribute(name, selectAll()); + return _getMatchingSelection(values, pred); +} + + //-------------------------------------------------------------------------------------------------- #define INSTANTIATE_TEMPLATE_METHODS(T) \ @@ -358,7 +379,9 @@ std::string Population::_dynamicsAttributeDataType(const std::string& name) cons const Selection&) const; \ template std::vector Population::getDynamicsAttribute(const std::string&, \ const Selection&, \ - const T&) const; + const T&) const; \ + template Selection Population::filterAttribute(const std::string&, \ + std::function pred) const; INSTANTIATE_TEMPLATE_METHODS(float) diff --git a/src/utils.h b/src/utils.h index 60a0eba2..2a5ab1d6 100644 --- a/src/utils.h +++ b/src/utils.h @@ -10,5 +10,22 @@ #pragma once #include +#include + +#include std::string readFile(const std::string& path); + +template +bbp::sonata::Selection _getMatchingSelection(const std::vector& values, UnaryPredicate pred) { + using bbp::sonata::Selection; + Selection::Values ids; + Selection::Value id = 0; + for (const auto& v : values) { + if (pred(v)) { + ids.push_back(id); + } + ++id; + } + return Selection::fromValues(ids); +} diff --git a/tests/data/node_sets.json b/tests/data/node_sets.json index 437fbef9..f433ef93 100644 --- a/tests/data/node_sets.json +++ b/tests/data/node_sets.json @@ -8,5 +8,14 @@ "model_type": "point", "node_id": [1, 2, 3, 5, 7, 9] }, + "power_number_test": { + "numeric_attribute_gt": { "$gt": 3 }, + "numeric_attribute_lt": { "$lt": 3 }, + "numeric_attribute_gte": { "$gte": 3 }, + "numeric_attribute_lte": { "$lte": 3 } + }, + "power_regex_test": { + "string_attr": { "$regex": "^[s][o]me value$" } + }, "combined": ["bio_layer45", "V1_point_prime"] } diff --git a/tests/test_node_sets.cpp b/tests/test_node_sets.cpp index 64aaae49..14b29013 100644 --- a/tests/test_node_sets.cpp +++ b/tests/test_node_sets.cpp @@ -56,6 +56,18 @@ TEST_CASE("NodeSetParse") { NodeSets ns(node_sets); CHECK_THROWS_AS(ns.materialize("NONEXISTANT", population), SonataError); } + + SECTION("OperatorMultipleClauses") + { + auto node_sets = R""({ "NodeSet0": {"attr-Y": {"has to ops": 3, "2nd": 3}} })""; + CHECK_THROWS_AS(NodeSets(node_sets), SonataError); + } + + SECTION("OperatorObject") + { + auto node_sets = R""({ "NodeSet0": {"attr-Y": {"has to ops": {}}} })""; + CHECK_THROWS_AS(NodeSets(node_sets), SonataError); + } } TEST_CASE("NodeSetBasic") { @@ -83,6 +95,57 @@ TEST_CASE("NodeSetBasic") { CHECK(sel == Selection({{0, 1}, {2, 3}, {4, 6}})); } + SECTION("BasicScalarOperatorStringRegex") { + { + auto node_sets = R"({ "NodeSet0": {"E-mapping-good": {"$regex": "^[AC].*"}} })"; + NodeSets ns(node_sets); + Selection sel = ns.materialize("NodeSet0", population); + CHECK(sel == Selection({{0, 1}, {2, 6}})); + } + + { + auto node_sets = R""({ "NodeSet0": {"attr-Z": {"$regex": "^(aa|bb|ff)"}} })""; + NodeSets ns(node_sets); + Selection sel = ns.materialize("NodeSet0", population); + CHECK(sel == Selection({{0, 2}, {5,6}})); + } + { + auto node_sets = R""({ "NodeSet0": {"attr-Z": {"$op-does-not-exist": "dne"}} })""; + CHECK_THROWS_AS(NodeSets(node_sets), SonataError); + } + } + + SECTION("BasicScalarOperatorNumeric") { + { + auto node_sets = R"({ "NodeSet0": {"attr-Y": {"$gt": 23}} })"; + NodeSets ns(node_sets); + Selection sel = ns.materialize("NodeSet0", population); + CHECK(sel == Selection({{3, 6}})); + } + { + auto node_sets = R"({ "NodeSet0": {"attr-Y": {"$lt": 23}} })"; + NodeSets ns(node_sets); + Selection sel = ns.materialize("NodeSet0", population); + CHECK(sel == Selection({{0, 2}})); + } + { + auto node_sets = R"({ "NodeSet0": {"attr-Y": {"$gte": 23}} })"; + NodeSets ns(node_sets); + Selection sel = ns.materialize("NodeSet0", population); + CHECK(sel == Selection({{2, 6}})); + } + { + auto node_sets = R"({ "NodeSet0": {"attr-Y": {"$lte": 23}} })"; + NodeSets ns(node_sets); + Selection sel = ns.materialize("NodeSet0", population); + CHECK(sel == Selection({{0, 3}})); + } + { + auto node_sets = R""({ "NodeSet0": {"attr-Y": {"$op-does-not-exist": 3}} })""; + CHECK_THROWS_AS(NodeSets(node_sets), SonataError); + } + } + SECTION("BasicScalarAnded") { auto node_sets = R"({"NodeSet0": {"E-mapping-good": "C", "attr-Y": [21, 22] @@ -176,6 +239,15 @@ TEST_CASE("NodeSet") { "model_type": "point", "node_id": [1, 2, 3, 5, 7, 9] }, + "power_number_test": { + "numeric_attribute_gt": { "$gt": 3 }, + "numeric_attribute_lt": { "$lt": 3 }, + "numeric_attribute_gte": { "$gte": 3 }, + "numeric_attribute_lte": { "$lte": 3 } + }, + "power_regex_test": { + "string_attr": { "$regex": "^[s][o]me value$" } + }, "combined": ["bio_layer45", "V1_point_prime"] })"; @@ -191,7 +263,7 @@ TEST_CASE("NodeSet") { SECTION("names") { NodeSets ns(node_sets); - std::set expected = {"bio_layer45", "V1_point_prime", "combined"}; + std::set expected = {"bio_layer45", "V1_point_prime", "combined", "power_number_test", "power_regex_test"}; CHECK(ns.names() == expected); } } diff --git a/tests/test_nodes.cpp b/tests/test_nodes.cpp index 952ead90..9b129eb0 100644 --- a/tests/test_nodes.cpp +++ b/tests/test_nodes.cpp @@ -177,6 +177,11 @@ TEST_CASE("NodePopulationmatchAttributeValues", "[base]") { auto sel1 = population.matchAttributeValues("E-mapping-good", std::string("does-not-exist")); CHECK(Selection({}) == sel1); + + std::vector strings {"C", "C", "C", "A", "B", "A"}; + auto sel2 = population.matchAttributeValues("E-mapping-good", strings); + CHECK(sel2.flatSize() == 6); + CHECK(Selection({{0, 6}}) == sel2); } SECTION("Float attribute") {