Skip to content

Commit de87e6c

Browse files
committed
feat(silo): add getDescendants function, add duplicate node error handling
1 parent 91aa8bc commit de87e6c

10 files changed

Lines changed: 304 additions & 6 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"testCaseName": "PhyloDescendentOf should return only descendent nodes of a given node",
3+
"query": {
4+
"action": {
5+
"type": "Aggregated",
6+
"groupByFields": ["gisaid_epi_isl"],
7+
"orderByFields": [
8+
{
9+
"field": "gisaid_epi_isl",
10+
"order": "ascending"
11+
}
12+
]
13+
},
14+
"filterExpression": {
15+
"type": "PhyloDescendentOf",
16+
"column": "gisaid_epi_isl",
17+
"searchExpression": "NODE_0000072"
18+
}
19+
},
20+
"expectedQueryResult": [
21+
{
22+
"count": 1,
23+
"gisaid_epi_isl": "EPI_ISL_1003849"
24+
},
25+
{
26+
"count": 1,
27+
"gisaid_epi_isl": "EPI_ISL_1260480"
28+
}
29+
]
30+
}

src/silo/common/phylo_tree.cpp

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ std::shared_ptr<TreeNode> parse_auspice_tree(
3434
node->children.push_back(child_node);
3535
}
3636

37+
if (node_map.find(node->node_id) != node_map.end()) {
38+
throw silo::preprocessing::PreprocessingException(
39+
fmt::format("Duplicate node ID found in Newick string: '{}'", node->node_id.string)
40+
);
41+
}
3742
node_map[node->node_id] = node;
3843
return node;
3944
}
@@ -149,6 +154,11 @@ std::shared_ptr<TreeNode> parseSubtree(
149154
node->node_id = parseLabel(sv);
150155
skipWhitespace(sv);
151156

157+
if (node_map.find(node->node_id) != node_map.end()) {
158+
throw silo::preprocessing::PreprocessingException(
159+
fmt::format("Duplicate node ID found in Newick string: '{}'", node->node_id.string)
160+
);
161+
}
152162
node_map[node->node_id] = node;
153163

154164
return node;
@@ -178,7 +188,7 @@ PhyloTree PhyloTree::fromNewickString(const std::string& newick_string) {
178188
}
179189
} catch (const std::exception& e) {
180190
throw silo::preprocessing::PreprocessingException(
181-
fmt::format("Error when parsing the Newick string: '{}'", newick_string)
191+
fmt::format("Error when parsing the Newick string '{}': {}", newick_string, e.what())
182192
);
183193
}
184194

@@ -206,7 +216,7 @@ PhyloTree PhyloTree::fromNewickFile(const std::filesystem::path& newick_path) {
206216
return fromNewickString(contents.str());
207217
} catch (const std::exception& e) {
208218
throw silo::preprocessing::PreprocessingException(
209-
fmt::format("Error when parsing the Newick file: '{}'", newick_path.string())
219+
fmt::format("Error when parsing the Newick file '{}': {}", newick_path.string(), e.what())
210220
);
211221
}
212222
}
@@ -216,14 +226,60 @@ PhyloTree PhyloTree::fromFile(const std::filesystem::path& path) {
216226

217227
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
218228

229+
if (ext != ".nwk" && ext != ".json") {
230+
throw silo::preprocessing::PreprocessingException(fmt::format(
231+
"Error when parsing tree file: '{}'. Path must end with .nwk or .json", path.string()
232+
));
233+
}
219234
if (ext == ".nwk") {
220235
return common::PhyloTree::fromNewickFile(path);
221236
} else if (ext == ".json") {
222237
return common::PhyloTree::fromAuspiceJSONFile(path);
223238
}
224-
throw silo::preprocessing::PreprocessingException(fmt::format(
225-
"Error when parsing tree file: '{}'. Path must end with .nwk or .json", path.string()
226-
));
239+
}
240+
241+
void PhyloTree::validateNodeExists(const TreeNodeId& node_id) {
242+
if (nodes.find(node_id) == nodes.end()) {
243+
throw silo::preprocessing::PreprocessingException(
244+
fmt::format("Node '{}' not found in the tree.", node_id.string)
245+
);
246+
}
247+
}
248+
249+
void PhyloTree::validateNodeExists(const std::string& node_label) {
250+
auto node_id = TreeNodeId{node_label};
251+
validateNodeExists(node_id);
252+
}
253+
254+
roaring::Roaring PhyloTree::getDescendants(const TreeNodeId& node_id) {
255+
validateNodeExists(node_id);
256+
auto child_it = nodes.find(node_id);
257+
roaring::Roaring result_bitmap;
258+
if (!child_it->second) {
259+
throw silo::preprocessing::PreprocessingException("Node is null.");
260+
}
261+
std::function<void(const std::shared_ptr<TreeNode>&)> dfs =
262+
[&](const std::shared_ptr<TreeNode>& current) {
263+
if (!current)
264+
return;
265+
if (current->isLeaf()) {
266+
if (current->row_index.has_value()) {
267+
result_bitmap.add(current->row_index.value());
268+
}
269+
}
270+
for (const auto& child : current->children) {
271+
dfs(child);
272+
}
273+
};
274+
if (child_it->second->isLeaf()) {
275+
return result_bitmap;
276+
}
277+
dfs(child_it->second);
278+
return result_bitmap;
279+
}
280+
281+
roaring::Roaring PhyloTree::getDescendants(const std::string& node_label) {
282+
return getDescendants(TreeNodeId{node_label});
227283
}
228284

229285
} // namespace silo::common

src/silo/common/phylo_tree.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,30 @@
66
#include <nlohmann/json.hpp>
77

88
#include "silo/common/tree_node_id.h"
9+
#include "silo/query_engine/batched_bitmap_reader.h"
910

1011
namespace silo::common {
1112
using silo::common::TreeNodeId;
1213

1314
class TreeNode {
1415
public:
1516
TreeNodeId node_id;
17+
std::optional<size_t> row_index; // index of corresponding sequence in the database (will be
18+
// empty for internal nodes)
1619
std::vector<std::shared_ptr<TreeNode>> children;
1720
std::optional<std::shared_ptr<TreeNode>> parent;
1821
int depth;
22+
23+
bool isLeaf() { return children.empty(); }
24+
bool rowIndexExists() const { return row_index.has_value(); }
1925
};
2026

2127
class PhyloTree {
2228
public:
2329
std::unordered_map<TreeNodeId, std::shared_ptr<TreeNode>> nodes;
2430

31+
// Functions for reading and parsing phylogenetic trees
32+
2533
static PhyloTree fromAuspiceJSONFile(const std::filesystem::path& json_path);
2634

2735
static PhyloTree fromAuspiceJSONString(const std::string& json_string);
@@ -31,6 +39,17 @@ class PhyloTree {
3139
static PhyloTree fromNewickString(const std::string& newick_string);
3240

3341
static PhyloTree fromFile(const std::filesystem::path& path);
42+
43+
// Functions for querying the phylogenetic tree
44+
45+
void validateNodeExists(const TreeNodeId& node_id);
46+
47+
void validateNodeExists(const std::string& node_label);
48+
49+
// returns a bitmap of all descendants node{node_id} that are also in the database
50+
roaring::Roaring getDescendants(const TreeNodeId& node_id);
51+
52+
roaring::Roaring getDescendants(const std::string& node_label);
3453
};
3554

3655
} // namespace silo::common

src/silo/common/phylo_tree.test.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,29 @@ TEST(PhyloTree, throwsOnInvalidJSON) {
5050
);
5151
}
5252

53+
TEST(PhyloTree, throwsOnInvalidAuspiceJSONDuplicateNodeId) {
54+
EXPECT_THROW(
55+
PhyloTree::fromAuspiceJSONString(R"({
56+
"version": "schema version",
57+
"meta": {},
58+
"tree": {
59+
"name": "ROOT",
60+
"children": [
61+
{
62+
"name": "CHILD",
63+
"children": [
64+
{
65+
"name": "CHILD"
66+
}
67+
]
68+
}
69+
]
70+
}
71+
})"),
72+
silo::preprocessing::PreprocessingException
73+
);
74+
}
75+
5376
TEST(PhyloTree, correctlyParsesFromNewick) {
5477
auto phylo_tree_file = PhyloTree::fromNewickString("((CHILD2)CHILD)ROOT;");
5578
ASSERT_EQ(phylo_tree_file.nodes.size(), 3);
@@ -95,3 +118,9 @@ TEST(PhyloTree, throwsOnInvalidNewickNoSemicolon) {
95118
silo::preprocessing::PreprocessingException
96119
);
97120
}
121+
122+
TEST(PhyloTree, throwsOnInvalidNewickWithDuplicateNodeId) {
123+
EXPECT_THROW(
124+
PhyloTree::fromNewickString("((CHILD)CHILD)ROOT"), silo::preprocessing::PreprocessingException
125+
);
126+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
#include "silo/query_engine/filter/expressions/phylo_tree_filter.h"
2+
3+
#include <optional>
4+
#include <utility>
5+
6+
#include <fmt/format.h>
7+
#include <nlohmann/json.hpp>
8+
9+
#include "silo/common/panic.h"
10+
#include "silo/common/string.h"
11+
#include "silo/database.h"
12+
#include "silo/query_engine/bad_request.h"
13+
#include "silo/query_engine/filter/expressions/expression.h"
14+
#include "silo/query_engine/filter/operators/bitmap_producer.h"
15+
#include "silo/query_engine/filter/operators/operator.h"
16+
#include "silo/storage/table_partition.h"
17+
18+
namespace silo::query_engine::filter::expressions {
19+
20+
PhyloChildFilter::PhyloChildFilter(std::string column_name, std::string internal_node)
21+
: column_name(std::move(column_name)),
22+
internal_node(std::move(internal_node)) {}
23+
24+
std::string PhyloChildFilter::toString() const {
25+
return fmt::format("column {} phylo_child_of {}", column_name, internal_node);
26+
};
27+
28+
namespace {
29+
std::unique_ptr<silo::query_engine::filter::operators::Operator> createMatchingBitmap(
30+
const storage::column::StringColumnPartition& string_column,
31+
const std::string& internal_node,
32+
size_t row_count
33+
) {
34+
return std::make_unique<operators::BitmapProducer>(
35+
[&, row_count]() {
36+
roaring::Roaring result_bitmap = string_column.getDescendants(internal_node);
37+
return CopyOnWriteBitmap(std::move(result_bitmap));
38+
},
39+
row_count
40+
);
41+
}
42+
43+
} // namespace
44+
45+
std::unique_ptr<silo::query_engine::filter::operators::Operator> PhyloChildFilter::compile(
46+
const Database& /*database*/,
47+
const storage::TablePartition& database_partition,
48+
Expression::AmbiguityMode /*mode*/
49+
) const {
50+
CHECK_SILO_QUERY(
51+
database_partition.columns.string_columns.contains(column_name),
52+
fmt::format("The database does not contain the string column '{}'", column_name)
53+
);
54+
55+
SILO_ASSERT(database_partition.columns.string_columns.contains(column_name));
56+
const auto& string_column = database_partition.columns.string_columns.at(column_name);
57+
return createMatchingBitmap(string_column, internal_node, database_partition.sequence_count);
58+
}
59+
60+
// NOLINTNEXTLINE(readability-identifier-naming)
61+
void from_json(const nlohmann::json& json, std::unique_ptr<PhyloChildFilter>& filter) {
62+
CHECK_SILO_QUERY(
63+
json.contains("column"), "The field 'column' is required in an PhyloChildFilter expression"
64+
)
65+
CHECK_SILO_QUERY(
66+
json["column"].is_string(),
67+
"The field 'column' in an PhyloChildFilter expression needs to be a string"
68+
)
69+
CHECK_SILO_QUERY(
70+
json.contains("internal_node"),
71+
"The field 'internal_node' is required in an PhyloChildFilter expression"
72+
)
73+
CHECK_SILO_QUERY(
74+
json["internal_node"].is_string(),
75+
"The field 'internal_node' in an PhyloChildFilter expression needs to be a string"
76+
)
77+
filter = std::make_unique<PhyloChildFilter>(
78+
json["column"].get<std::string>(), json["internal_node"].get<std::string>()
79+
);
80+
}
81+
82+
} // namespace silo::query_engine::filter::expressions
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#pragma once
2+
3+
#include <memory>
4+
#include <string>
5+
6+
#include <nlohmann/json_fwd.hpp>
7+
8+
#include "silo/database.h"
9+
#include "silo/query_engine/filter/expressions/expression.h"
10+
#include "silo/query_engine/filter/operators/operator.h"
11+
#include "silo/storage/table_partition.h"
12+
13+
namespace silo::query_engine::filter::expressions {
14+
15+
class PhyloChildFilter : public Expression {
16+
std::string column_name;
17+
std::string internal_node;
18+
19+
public:
20+
explicit PhyloChildFilter(std::string column_name, std::string internal_node);
21+
22+
std::string toString() const override;
23+
24+
[[nodiscard]] std::unique_ptr<silo::query_engine::filter::operators::Operator> compile(
25+
const Database& database,
26+
const storage::TablePartition& database_partition,
27+
AmbiguityMode mode
28+
) const override;
29+
30+
private:
31+
std::optional<const roaring::Roaring*> getBitmapForValue(
32+
const silo::storage::column::StringColumnPartition& phylo_tree_index_column
33+
) const;
34+
};
35+
36+
// NOLINTNEXTLINE(readability-identifier-naming)
37+
void from_json(const nlohmann::json& json, std::unique_ptr<PhyloChildFilter>& filter);
38+
39+
} // namespace silo::query_engine::filter::expressions

src/silo/storage/column/string_column.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44

55
#include "silo/common/bidirectional_map.h"
66
#include "silo/common/string.h"
7+
#include "silo/common/tree_node_id.h"
8+
#include "silo/initialize/initialize_exception.h"
79

810
using silo::common::String;
911
using silo::common::STRING_SIZE;
12+
using silo::common::TreeNodeId;
1013

1114
namespace silo::storage::column {
1215

@@ -21,6 +24,18 @@ StringColumnPartition::StringColumnPartition(StringColumnMetadata* metadata)
2124
void StringColumnPartition::insert(const std::string& value) {
2225
const String<STRING_SIZE> tmp(value, metadata->dictionary);
2326
values.push_back(tmp);
27+
if (metadata->phylo_tree.has_value()) {
28+
auto child_it = (metadata->phylo_tree->nodes).find(TreeNodeId{value});
29+
if (child_it == metadata->phylo_tree->nodes.end()) {
30+
return;
31+
}
32+
if (child_it->second->rowIndexExists()) {
33+
throw silo::initialize::InitializeException(
34+
fmt::format("Node '{}' already exists in the phylogenetic tree.", value)
35+
);
36+
}
37+
child_it->second->row_index = values.size() - 1;
38+
}
2439
}
2540

2641
void StringColumnPartition::insertNull() {

0 commit comments

Comments
 (0)