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

BlueBrain / libsonata / 4542177729

pending completion
4542177729

push

github

Mike Gevaert
review comments

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

1689 of 1734 relevant lines covered (97.4%)

70.99 hits per line

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

98.82
/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_int64_or_throw(const json& el) {
76✔
349
    auto v = el.get<double>();
76✔
350
    if (std::floor(v) != v) {
76✔
351
        throw SonataError(fmt::format("expected integer, got float {}", v));
×
352
    }
353
    return static_cast<int64_t>(v);
76✔
354
}
355

356
uint64_t get_uint64_or_throw(const json& el) {
6✔
357
    auto v = el.get<double>();
6✔
358
    if (v < 0) {
6✔
359
        throw SonataError(fmt::format("expected unsigned integer, got {}", v));
4✔
360
    }
361

362
    if (std::floor(v) != v) {
4✔
363
        throw SonataError(fmt::format("expected integer, got float {}", v));
4✔
364
    }
365
    return static_cast<uint64_t>(v);
2✔
366
}
367

368
NodeSetRulePtr _dispatch_node(const std::string& attribute, const json& value) {
148✔
369
    if (value.is_number()) {
148✔
370
        if (attribute == "population") {
12✔
371
            throw SonataError("'population' must be a string");
2✔
372
        }
373

374
        if (attribute == "node_id") {
10✔
375
            Selection::Values node_ids{get_uint64_or_throw(value)};
6✔
376
            return std::make_unique<NodeSetBasicNodeIds>(std::move(node_ids));
2✔
377
        } else {
378
            std::vector<int64_t> f = {get_int64_or_throw(value)};
4✔
379
            return std::make_unique<NodeSetBasicRule<int64_t>>(attribute, f);
4✔
380
        }
381
    } else if (value.is_string()) {
136✔
382
        if (attribute == "node_id") {
28✔
383
            throw SonataError("'node_id' must be numeric or a list of numbers");
2✔
384
        }
385

386
        if (attribute == "population") {
26✔
387
            std::vector<std::string> v{value.get<std::string>()};
30✔
388
            return std::make_unique<NodeSetBasicPopulation>(v);
10✔
389
        } else {
390
            std::vector<std::string> f = {value.get<std::string>()};
48✔
391
            return std::make_unique<NodeSetBasicRule<std::string>>(attribute, f);
16✔
392
        }
393
    } else if (value.is_array()) {
108✔
394
        const auto& array = value;
48✔
395

396
        if (array.empty()) {
48✔
397
            throw SonataError(fmt::format("NodeSet Array is empty for attribute: {}", attribute));
×
398
        }
399

400
        if (array[0].is_number()) {
48✔
401
            if (attribute == "population") {
24✔
402
                throw SonataError("'population' must be a string");
2✔
403
            }
404

405
            std::vector<int64_t> values;
44✔
406
            for (auto& inner_el : array.items()) {
94✔
407
                values.emplace_back(get_int64_or_throw(inner_el.value()));
72✔
408
            }
409

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

430
            std::vector<std::string> values;
40✔
431
            for (auto& inner_el : array.items()) {
54✔
432
                values.emplace_back(inner_el.value().get<std::string>());
34✔
433
            }
434

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

452
        if (value.is_number()) {
58✔
453
            return std::make_unique<NodeSetBasicOperatorNumeric>(attribute,
40✔
454
                                                                 key,
455
                                                                 value.get<double>());
82✔
456
        } else if (value.is_string()) {
16✔
457
            return std::make_unique<NodeSetBasicOperatorString>(attribute,
26✔
458
                                                                key,
459
                                                                value.get<std::string>());
42✔
460
        } else {
461
            throw SonataError("Unknown operator");
2✔
462
        }
463
    } else {
464
        THROW_IF_REACHED  // LCOV_EXCL_LINE
465
    }
466
}
467

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

493
void check_compound(const std::map<std::string, NodeSetRulePtr>& node_sets,
76✔
494
                    const std::map<std::string, CompoundTargets>& compound_rules,
495
                    const std::string& name,
496
                    size_t depth) {
497
    if (node_sets.count(name) > 0) {
76✔
498
        return;
28✔
499
    }
500

501
    if (depth > MAX_COMPOUND_RECURSION) {
48✔
502
        throw SonataError("Compound node_set recursion depth exceeded");
2✔
503
    }
504

505
    const auto it = compound_rules.find(name);
46✔
506
    assert(it != compound_rules.end());
46✔
507

508
    for (auto const& target : it->second) {
74✔
509
        if (node_sets.count(target) == 0 && compound_rules.count(target) == 0) {
56✔
510
            throw SonataError(fmt::format("Missing '{}' from node_sets", target));
8✔
511
        }
512
        check_compound(node_sets, compound_rules, target, depth + 1);
52✔
513
    }
514
}
515

516
void parse_compound(const json& j, std::map<std::string, NodeSetRulePtr>& node_sets) {
52✔
517
    std::map<std::string, CompoundTargets> compound_rules;
104✔
518
    for (auto& el : j.items()) {
156✔
519
        if (el.value().is_array()) {
102✔
520
            CompoundTargets targets;
56✔
521
            for (const auto& name : el.value()) {
66✔
522
                if (!name.is_string()) {
40✔
523
                    throw SonataError("All compound elements must be strings");
2✔
524
                }
525

526
                targets.emplace_back(name);
38✔
527
            }
528
            compound_rules[el.key()] = targets;
26✔
529
        }
530
    }
531

532

533
    for (const auto& rule : compound_rules) {
68✔
534
        check_compound(node_sets, compound_rules, rule.first, 0);
24✔
535

536
        NodeSetRulePtr rules = std::make_unique<NodeSetCompoundRule>(rule.first, rule.second);
36✔
537
        node_sets.emplace(rule.first, std::move(rules));
18✔
538
    }
539
}
44✔
540

541
}  // namespace detail
542

543
NodeSets::NodeSets(const std::string& content)
80✔
544
    : impl_(new detail::NodeSets(content)) {}
80✔
545

546
NodeSets::NodeSets(NodeSets&&) noexcept = default;
547
NodeSets& NodeSets::operator=(NodeSets&&) noexcept = default;
548
NodeSets::~NodeSets() = default;
549

550
NodeSets NodeSets::fromFile(const std::string& path) {
2✔
551
    const auto contents = readFile(path);
4✔
552
    return NodeSets(contents);
4✔
553
}
554

555
Selection NodeSets::materialize(const std::string& name, const NodePopulation& population) const {
44✔
556
    return impl_->materialize(name, population);
44✔
557
}
558

559
std::set<std::string> NodeSets::names() const {
2✔
560
    return impl_->names();
2✔
561
}
562

563
std::string NodeSets::toJSON() const {
10✔
564
    return impl_->toJSON();
10✔
565
}
566

567
}  // namespace sonata
568
}  // 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