• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

BlueBrain / libsonata / 4540481104

pending completion
4540481104

push

github

Mike Gevaert
cleanup nodesets so we're explicit about multi-clauses

102 of 102 new or added lines in 1 file covered. (100.0%)

1686 of 1730 relevant lines covered (97.46%)

71.15 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.2
/src/node_sets.cpp
1
#include <algorithm>  // std::find, std::transform
2
#include <cassert>
3
#include <cmath>
4
#include <fmt/format.h>
5
#include <sstream>
6
#include <type_traits>
7

8
#include <nlohmann/json.hpp>
9
#include <utility>
10

11
#include "utils.h"  // readFile
12

13
#include <bbp/sonata/node_sets.h>
14

15
namespace bbp {
16
namespace sonata {
17

18
namespace detail {
19

20
const size_t MAX_COMPOUND_RECURSION = 10;
21

22
using json = nlohmann::json;
23

24
void replace_trailing_coma(std::string& s, char c) {
40✔
25
    s.pop_back();
40✔
26
    s.pop_back();
40✔
27
    s.push_back(c);
40✔
28
}
40✔
29

30
template <typename T>
31
std::string toString(const std::string& key, const std::vector<T>& values) {
10✔
32
    return fmt::format(R"("{}": [{}])", key, fmt::join(values, ", "));
20✔
33
}
34

35
template <>
36
std::string toString(const std::string& key, const std::vector<std::string>& values) {
50✔
37
    // strings need to be wrapped in quotes
38
    return fmt::format(R"("{}": ["{}"])", key, fmt::join(values, "\", \""));
100✔
39
}
40

41
class NodeSets;
42

43
class NodeSetRule
44
{
45
  public:
46
    virtual ~NodeSetRule() = default;
172✔
47

48
    virtual Selection materialize(const NodeSets&, const NodePopulation&) const = 0;
49
    virtual std::string toJSON() const = 0;
50
};
51

52
using NodeSetRulePtr = std::unique_ptr<NodeSetRule>;
53
void parse_basic(const json& j, std::map<std::string, NodeSetRulePtr>& node_sets);
54
void parse_compound(const json& j, std::map<std::string, NodeSetRulePtr>& node_sets);
55

56
class NodeSets
57
{
58
    std::map<std::string, NodeSetRulePtr> node_sets_;
59

60
  public:
61
    explicit NodeSets(const std::string& content) {
116✔
62
        json j = json::parse(content);
160✔
63
        if (!j.is_object()) {
80✔
64
            throw SonataError("Top level node_set must be an object");
4✔
65
        }
66

67
        // Need to two pass parsing the json so that compound lookup can rely
68
        // on all the basic rules existing
69
        parse_basic(j, node_sets_);
76✔
70
        parse_compound(j, node_sets_);
52✔
71
    }
44✔
72

73
    Selection materialize(const std::string& name, const NodePopulation& population) const {
104✔
74
        const auto& node_set = node_sets_.find(name);
104✔
75
        if (node_set == node_sets_.end()) {
104✔
76
            throw SonataError(fmt::format("Unknown node_set {}", name));
4✔
77
        }
78

79
        Selection ret = population.selectAll() & node_set->second->materialize(*this, population);
204✔
80
        return ret;
204✔
81
    }
82

83

84
    std::set<std::string> names() const {
2✔
85
        return getMapKeys(node_sets_);
2✔
86
    }
87

88
    std::string toJSON() const {
10✔
89
        std::string ret{"{\n"};
10✔
90
        for (const auto& pair : node_sets_) {
60✔
91
            ret += fmt::format(R"(  "{}": {{)", pair.first);
100✔
92
            ret += pair.second->toJSON();
50✔
93
            ret += "},\n";
50✔
94
        }
95
        replace_trailing_coma(ret, '\n');
10✔
96
        ret += "}";
10✔
97

98
        return ret;
10✔
99
    }
100
};
101

102
// { 'region': ['region1', 'region2', ...] }
103
template <typename T>
104
class NodeSetBasicRule: public NodeSetRule
105
{
106
  public:
107
    NodeSetBasicRule(std::string attribute, std::vector<T>& values)
40✔
108
        : attribute_(std::move(attribute))
40✔
109
        , values_(values) {}
40✔
110

111
    Selection materialize(const detail::NodeSets& /* unused */,
10✔
112
                          const NodePopulation& np) const final {
113
        return np.matchAttributeValues(attribute_, values_);
10✔
114
    }
115

116
    std::string toJSON() const final {
38✔
117
        return toString(attribute_, values_);
38✔
118
    }
119

120
  private:
121
    std::string attribute_;
122
    std::vector<T> values_;
123
};
124

125
// { 'population': ['popA', 'popB', ] }
126
class NodeSetBasicPopulation: public NodeSetRule
127
{
128
  public:
129
    explicit NodeSetBasicPopulation(std::vector<std::string>& values)
14✔
130
        : values_(values) {}
14✔
131

132
    Selection materialize(const detail::NodeSets& /* unused */,
6✔
133
                          const NodePopulation& np) const final {
134
        if (std::find(values_.begin(), values_.end(), np.name()) != values_.end()) {
6✔
135
            return np.selectAll();
4✔
136
        }
137

138
        return Selection{{}};
2✔
139
    }
140

141
    std::string toJSON() const final {
10✔
142
        return toString("population", values_);
10✔
143
    }
144

145
  private:
146
    std::vector<std::string> values_;
147
};
148

149
// { 'node_id': [1, 2, 3, 4] }
150
class NodeSetBasicNodeIds: public NodeSetRule
151
{
152
  public:
153
    explicit NodeSetBasicNodeIds(Selection::Values&& values)
18✔
154
        : values_(values) {}
18✔
155

156
    Selection materialize(const detail::NodeSets& /* unused */,
36✔
157
                          const NodePopulation& np) const final {
158
        return np.selectAll() & Selection::fromValues(values_.begin(), values_.end());
72✔
159
    }
160

161
    std::string toJSON() const final {
6✔
162
        return toString("node_ids", values_);
6✔
163
    }
164

165
  private:
166
    Selection::Values values_;
167
};
168

169
//  {
170
//      "population": "biophysical",
171
//      "model_type": "point",
172
//      "node_id": [1, 2, 3, 5, 7, 9, ...]
173
//  }
174
class NodeSetBasicMultiClause: public NodeSetRule
175
{
176
  public:
177
    explicit NodeSetBasicMultiClause(std::vector<NodeSetRulePtr>&& clauses)
26✔
178
        : clauses_(std::move(clauses)) {}
26✔
179

180
    Selection materialize(const detail::NodeSets& ns, const NodePopulation& np) const final {
2✔
181
        Selection ret = np.selectAll();
2✔
182
        for (const auto& clause : clauses_) {
6✔
183
            ret = ret & clause->materialize(ns, np);
4✔
184
        }
185
        return ret;
2✔
186
    }
187

188
    std::string toJSON() const final {
30✔
189
        std::string ret;
30✔
190
        for (const auto& clause : clauses_) {
120✔
191
            ret += clause->toJSON();
90✔
192
            ret += ", ";
90✔
193
        }
194
        replace_trailing_coma(ret, ' ');
30✔
195
        return ret;
30✔
196
    }
197

198
  private:
199
    std::vector<NodeSetRulePtr> clauses_;
200
};
201

202
// "string_attr": { "$regex": "^[s][o]me value$" }
203
class NodeSetBasicOperatorString: public NodeSetRule
204
{
205
  public:
206
    explicit NodeSetBasicOperatorString(std::string attribute,
14✔
207
                                        const std::string& op,
208
                                        std::string value)
209
        : op_(string2op(op))
40✔
210
        , attribute_(std::move(attribute))
12✔
211
        , value_(std::move(value)) {}
14✔
212

213
    Selection materialize(const detail::NodeSets& /* unused */,
4✔
214
                          const NodePopulation& np) const final {
215
        switch (op_) {
4✔
216
        case Op::regex:
4✔
217
            return np.regexMatch(attribute_, value_);
4✔
218
        default:              // LCOV_EXCL_LINE
219
            THROW_IF_REACHED  // LCOV_EXCL_LINE
220
        }
221
    }
222

223
    std::string toJSON() const final {
10✔
224
        return fmt::format(R"("{}": {{ "{}": "{}" }})", attribute_, op2string(op_), value_);
20✔
225
    }
226

227
    enum class Op {
228
        regex = 1,
229
    };
230

231
    static Op string2op(const std::string& s) {
14✔
232
        if (s == "$regex") {
14✔
233
            return Op::regex;
12✔
234
        }
235
        throw SonataError(fmt::format("Operator '{}' not available for strings", s));
4✔
236
    }
237

238
    static std::string op2string(const Op op) {
10✔
239
        switch (op) {
10✔
240
        case Op::regex:
10✔
241
            return "$regex";
10✔
242
        default:              // LCOV_EXCL_LINE
243
            THROW_IF_REACHED  // LCOV_EXCL_LINE
244
        }
245
    }
246

247
  private:
248
    Op op_;
249
    std::string attribute_;
250
    std::string value_;
251
};
252

253
// "numeric_attribute_gt": { "$gt": 3 },
254
class NodeSetBasicOperatorNumeric: public NodeSetRule
255
{
256
  public:
257
    explicit NodeSetBasicOperatorNumeric(std::string name, const std::string& op, double value)
42✔
258
        : name_(std::move(name))
84✔
259
        , value_(value)
260
        , op_(string2op(op)) {}
44✔
261

262
    Selection materialize(const detail::NodeSets& /* unused */,
8✔
263
                          const NodePopulation& np) const final {
264
        switch (op_) {
8✔
265
        case Op::gt:
2✔
266
            return np.filterAttribute<double>(name_, [=](const double v) { return v > value_; });
14✔
267
        case Op::lt:
2✔
268
            return np.filterAttribute<double>(name_, [=](const double v) { return v < value_; });
14✔
269
        case Op::gte:
2✔
270
            return np.filterAttribute<double>(name_, [=](const double v) { return v >= value_; });
14✔
271
        case Op::lte:
2✔
272
            return np.filterAttribute<double>(name_, [=](const double v) { return v <= value_; });
14✔
273
        default:              // LCOV_EXCL_LINE
274
            THROW_IF_REACHED  // LCOV_EXCL_LINE
275
        }
276
    }
277

278
    std::string toJSON() const final {
40✔
279
        return fmt::format(R"("{}": {{ "{}": {} }})", name_, op2string(op_), value_);
80✔
280
    }
281

282
    enum class Op {
283
        gt = 1,
284
        lt = 2,
285
        gte = 3,
286
        lte = 4,
287
    };
288

289
    static Op string2op(const std::string& s) {
42✔
290
        if (s == "$gt") {
42✔
291
            return Op::gt;
10✔
292
        } else if (s == "$lt") {
32✔
293
            return Op::lt;
10✔
294
        } else if (s == "$gte") {
22✔
295
            return Op::gte;
10✔
296
        } else if (s == "$lte") {
12✔
297
            return Op::lte;
10✔
298
        }
299
        throw SonataError(fmt::format("Operator '{}' not available for numeric", s));
4✔
300
    }
301

302
    static std::string op2string(const Op op) {
40✔
303
        switch (op) {
40✔
304
        case Op::gt:
10✔
305
            return "$gt";
10✔
306
        case Op::lt:
10✔
307
            return "$lt";
10✔
308
        case Op::gte:
10✔
309
            return "$gte";
10✔
310
        case Op::lte:
10✔
311
            return "$lte";
10✔
312
        default:              // LCOV_EXCL_LINE
313
            THROW_IF_REACHED  // LCOV_EXCL_LINE
314
        }
315
    }
316

317
  private:
318
    std::string name_;
319
    double value_;
320
    Op op_;
321
};
322

323
using CompoundTargets = std::vector<std::string>;
324
class NodeSetCompoundRule: public NodeSetRule
325
{
326
  public:
327
    NodeSetCompoundRule(std::string name, CompoundTargets targets)
18✔
328
        : name_(std::move(name))
36✔
329
        , targets_(std::move(targets)) {}
18✔
330

331
    Selection materialize(const detail::NodeSets& ns, const NodePopulation& np) const final {
40✔
332
        Selection ret{{}};
40✔
333
        for (const auto& target : targets_) {
100✔
334
            ret = ret | ns.materialize(target, np);
60✔
335
        }
336
        return ret;
40✔
337
    }
338

339
    std::string toJSON() const final {
6✔
340
        return toString("node_ids", targets_);
6✔
341
    }
342

343
  private:
344
    std::string name_;
345
    CompoundTargets targets_;
346
};
347

348
int64_t get_integer_or_throw(const json& el) {
82✔
349
    auto v = el.get<double>();
82✔
350
    if (std::floor(v) != v) {
82✔
351
        throw SonataError("Only allowed integers in node set rules");
2✔
352
    }
353
    return static_cast<int64_t>(v);
80✔
354
}
355

356
NodeSetRulePtr _dispatch_node(const std::string& attribute, const json& value) {
148✔
357
    if (value.is_number()) {
148✔
358
        if (attribute == "population") {
12✔
359
            throw SonataError("'population' must be a string");
2✔
360
        }
361

362
        int64_t v = get_integer_or_throw(value);
10✔
363
        if (attribute == "node_id") {
8✔
364
            if (v < 0) {
4✔
365
                throw SonataError("'node_id' must be positive");
2✔
366
            }
367

368
            Selection::Values node_ids{static_cast<uint64_t>(v)};
2✔
369
            return std::make_unique<NodeSetBasicNodeIds>(std::move(node_ids));
2✔
370
        } else {
371
            std::vector<int64_t> f = {v};
4✔
372
            return std::make_unique<NodeSetBasicRule<int64_t>>(attribute, f);
4✔
373
        }
374
    } else if (value.is_string()) {
136✔
375
        if (attribute == "node_id") {
28✔
376
            throw SonataError("'node_id' must be numeric or a list of numbers");
2✔
377
        }
378

379
        if (attribute == "population") {
26✔
380
            std::vector<std::string> v{value.get<std::string>()};
30✔
381
            return std::make_unique<NodeSetBasicPopulation>(v);
10✔
382
        } else {
383
            std::vector<std::string> f = {value.get<std::string>()};
48✔
384
            return std::make_unique<NodeSetBasicRule<std::string>>(attribute, f);
16✔
385
        }
386
    } else if (value.is_array()) {
108✔
387
        const auto& array = value;
48✔
388

389
        if (array.empty()) {
48✔
390
            throw SonataError(fmt::format("NodeSet Array is empty for attribute: {}", attribute));
×
391
        }
392

393
        if (array[0].is_number()) {
48✔
394
            if (attribute == "population") {
24✔
395
                throw SonataError("'population' must be a string");
2✔
396
            }
397

398
            std::vector<int64_t> values;
44✔
399
            for (auto& inner_el : array.items()) {
94✔
400
                values.emplace_back(get_integer_or_throw(inner_el.value()));
72✔
401
            }
402

403
            if (attribute == "node_id") {
22✔
404
                Selection::Values node_ids;
20✔
405
                std::transform(begin(values),
406
                               end(values),
407
                               back_inserter(node_ids),
408
                               [](int64_t integer) {
56✔
409
                                   if (integer < 0) {
56✔
410
                                       throw SonataError("'node_id' must be positive");
2✔
411
                                   }
412
                                   return static_cast<Selection::Value>(integer);
54✔
413
                               });
18✔
414
                return std::make_unique<NodeSetBasicNodeIds>(std::move(node_ids));
16✔
415
            } else {
416
                return std::make_unique<NodeSetBasicRule<int64_t>>(attribute, values);
4✔
417
            }
418
        } else if (array[0].is_string()) {
24✔
419
            if (attribute == "node_id") {
22✔
420
                throw SonataError("'node_id' must be numeric or a list of numbers");
2✔
421
            }
422

423
            std::vector<std::string> values;
40✔
424
            for (auto& inner_el : array.items()) {
54✔
425
                values.emplace_back(inner_el.value().get<std::string>());
34✔
426
            }
427

428
            if (attribute == "population") {
20✔
429
                return std::make_unique<NodeSetBasicPopulation>(values);
4✔
430
            } else {
431
                return std::make_unique<NodeSetBasicRule<std::string>>(attribute, values);
16✔
432
            }
433
        } else {
434
            throw SonataError("Unknown array type");
2✔
435
        }
436
    } else if (value.is_object()) {
60✔
437
        const auto& definition = value;
60✔
438
        if (definition.size() != 1) {
60✔
439
            throw SonataError(
440
                fmt::format("Operator '{}' must have object with one key value pair", attribute));
4✔
441
        }
442
        const auto& key = definition.begin().key();
58✔
443
        const auto& value = definition.begin().value();
58✔
444

445
        if (value.is_number()) {
58✔
446
            return std::make_unique<NodeSetBasicOperatorNumeric>(attribute,
40✔
447
                                                                 key,
448
                                                                 value.get<double>());
82✔
449
        } else if (value.is_string()) {
16✔
450
            return std::make_unique<NodeSetBasicOperatorString>(attribute,
26✔
451
                                                                key,
452
                                                                value.get<std::string>());
42✔
453
        } else {
454
            throw SonataError("Unknown operator");
2✔
455
        }
456
    } else {
457
        THROW_IF_REACHED  // LCOV_EXCL_LINE
458
    }
459
}
460

461
void parse_basic(const json& j, std::map<std::string, NodeSetRulePtr>& node_sets) {
76✔
462
    for (const auto& el : j.items()) {
226✔
463
        const auto& value = el.value();
126✔
464
        if (value.is_object()) {
126✔
465
            if (value.empty()) {
98✔
466
                // ignore
467
            } else if (value.size() == 1) {
98✔
468
                const auto& inner_el = value.items().begin();
96✔
469
                node_sets[el.key()] = _dispatch_node(inner_el.key(), inner_el.value());
72✔
470
            } else {
471
                std::vector<NodeSetRulePtr> clauses;
26✔
472
                for (const auto& inner_el : value.items()) {
102✔
473
                    clauses.push_back(_dispatch_node(inner_el.key(), inner_el.value()));
76✔
474
                }
475
                node_sets[el.key()] = std::make_unique<NodeSetBasicMultiClause>(std::move(clauses));
26✔
476
            }
477
        } else if (value.is_array()) {
28✔
478
            // will be parsed by the parse_compound
479
        } else {
480
            // null/boolean/number/string ?
481
            throw SonataError(fmt::format("Expected an array or an object, got: {}", value.dump()));
×
482
        }
483
    }
484
}
52✔
485

486
void check_compound(const std::map<std::string, NodeSetRulePtr>& node_sets,
76✔
487
                    const std::map<std::string, CompoundTargets>& compound_rules,
488
                    const std::string& name,
489
                    size_t depth) {
490
    if (node_sets.count(name) > 0) {
76✔
491
        return;
28✔
492
    }
493

494
    if (depth > MAX_COMPOUND_RECURSION) {
48✔
495
        throw SonataError("Compound node_set recursion depth exceeded");
2✔
496
    }
497

498
    const auto it = compound_rules.find(name);
46✔
499
    assert(it != compound_rules.end());
46✔
500

501
    for (auto const& target : it->second) {
74✔
502
        if (node_sets.count(target) == 0 && compound_rules.count(target) == 0) {
56✔
503
            throw SonataError(fmt::format("Missing '{}' from node_sets", target));
8✔
504
        }
505
        check_compound(node_sets, compound_rules, target, depth + 1);
52✔
506
    }
507
}
508

509
void parse_compound(const json& j, std::map<std::string, NodeSetRulePtr>& node_sets) {
52✔
510
    std::map<std::string, CompoundTargets> compound_rules;
104✔
511
    for (auto& el : j.items()) {
156✔
512
        if (el.value().is_array()) {
102✔
513
            CompoundTargets targets;
56✔
514
            for (const auto& name : el.value()) {
66✔
515
                if (!name.is_string()) {
40✔
516
                    throw SonataError("All compound elements must be strings");
2✔
517
                }
518

519
                targets.emplace_back(name);
38✔
520
            }
521
            compound_rules[el.key()] = targets;
26✔
522
        }
523
    }
524

525

526
    for (const auto& rule : compound_rules) {
68✔
527
        check_compound(node_sets, compound_rules, rule.first, 0);
24✔
528

529
        NodeSetRulePtr rules = std::make_unique<NodeSetCompoundRule>(rule.first, rule.second);
36✔
530
        node_sets.emplace(rule.first, std::move(rules));
18✔
531
    }
532
}
44✔
533

534
}  // namespace detail
535

536
NodeSets::NodeSets(const std::string& content)
80✔
537
    : impl_(new detail::NodeSets(content)) {}
80✔
538

539
NodeSets::NodeSets(NodeSets&&) noexcept = default;
540
NodeSets& NodeSets::operator=(NodeSets&&) noexcept = default;
541
NodeSets::~NodeSets() = default;
542

543
NodeSets NodeSets::fromFile(const std::string& path) {
2✔
544
    const auto contents = readFile(path);
4✔
545
    return NodeSets(contents);
4✔
546
}
547

548
Selection NodeSets::materialize(const std::string& name, const NodePopulation& population) const {
44✔
549
    return impl_->materialize(name, population);
44✔
550
}
551

552
std::set<std::string> NodeSets::names() const {
2✔
553
    return impl_->names();
2✔
554
}
555

556
std::string NodeSets::toJSON() const {
10✔
557
    return impl_->toJSON();
10✔
558
}
559

560
}  // namespace sonata
561
}  // namespace bbp
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc