Skip to content

Commit 6220467

Browse files
authored
Merge pull request #149 from Mik1810/codex/remove-nodes-from-api
Remove nodes from api
2 parents c3c44ca + e936083 commit 6220467

4 files changed

Lines changed: 118 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ This project starts explicit release versioning with `0.4.1`. Older entries belo
44

55
Repository releases follow the compatibility policy in [`VERSIONING.md`](VERSIONING.md).
66

7+
## [1.3.41] - 2026-05-07
8+
9+
- Closed `#125` by adding `Graph::remove_nodes_from(...)` for batched node removal with a single vertex-map rebuild.
10+
- Preserved `remove_node(...)`'s missing-node error policy, ignored duplicate IDs in the batch, and covered mixed existing/missing inputs.
11+
- Optimized incident edge-attribute cleanup to scan only edges incident to the removed vertex.
12+
713
## [1.3.40] - 2026-05-07
814

915
- Closed `#95` by marking pure-query graph and result accessors as `[[nodiscard]]`.

RELEASE_NOTES.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
33
These notes are written for GitHub releases and can be more narrative than the
44
version entries in `CHANGELOG.md`.
55

6+
## [1.3.41]
7+
8+
### Highlights
9+
10+
- Closed `#125` by adding `Graph::remove_nodes_from(...)` for batched node
11+
removal.
12+
- The new API validates the full input first, follows `remove_node(...)`'s
13+
missing-node error policy, ignores duplicate IDs, and rebuilds wrapper-side
14+
vertex maps once after the batch.
15+
- Incident edge-attribute cleanup now scans the removed vertex's incident edges
16+
instead of the whole graph.
17+
618
## [1.3.40]
719

820
### Highlights

include/nxpp/graph.hpp

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,15 @@ class Graph {
505505
return v;
506506
}
507507

508+
std::optional<VertexDesc> find_vertex_by_id(const NodeID& id) const {
509+
for (auto [v, vend] = boost::vertices(g); v != vend; ++v) {
510+
if (boost::get(vertex_name_map, *v) == id) {
511+
return *v;
512+
}
513+
}
514+
return std::nullopt;
515+
}
516+
508517
std::size_t get_edge_id(EdgeDesc e) const {
509518
return boost::get(edge_id_map, e);
510519
}
@@ -549,9 +558,14 @@ class Graph {
549558

550559
void erase_incident_edge_properties(VertexDesc v) {
551560
std::vector<std::size_t> edge_ids;
552-
for (auto [e, eend] = boost::edges(g); e != eend; ++e) {
553-
if (boost::source(*e, g) == v || boost::target(*e, g) == v) {
554-
edge_ids.push_back(get_edge_id(*e));
561+
for (auto [e, eend] = boost::out_edges(v, g); e != eend; ++e) {
562+
edge_ids.push_back(get_edge_id(*e));
563+
}
564+
if constexpr (Directed) {
565+
for (auto [e, eend] = boost::in_edges(v, g); e != eend; ++e) {
566+
if (boost::source(*e, g) != v) {
567+
edge_ids.push_back(get_edge_id(*e));
568+
}
555569
}
556570
}
557571
for (auto edge_id : edge_ids) {
@@ -1004,8 +1018,8 @@ class Graph {
10041018
* state, erases the vertex, and rebuilds the internal `NodeID` to
10051019
* descriptor and vertex-index maps. Calling this in a tight loop over many
10061020
* node removals can add up to roughly O(V^2 + V*E) total work. Prefer
1007-
* building a filtered copy of the graph when you need to drop many
1008-
* vertices at once, or use `clear()` to reset a graph entirely.
1021+
* remove_nodes_from() when you need to drop many vertices at once, or use
1022+
* `clear()` to reset a graph entirely.
10091023
*
10101024
* @throws std::runtime_error If the node is not present.
10111025
*/
@@ -1025,6 +1039,52 @@ class Graph {
10251039
rebuild_vertex_maps();
10261040
}
10271041

1042+
/**
1043+
* @brief Removes a batch of nodes and all of their incident edges.
1044+
*
1045+
* Duplicate node IDs in the input are ignored after the first occurrence.
1046+
* Missing node IDs follow remove_node()'s error policy: the call throws
1047+
* before mutating the graph if any requested node is absent.
1048+
*
1049+
* This rebuilds wrapper-side vertex maps once after the full batch.
1050+
*
1051+
* @throws std::runtime_error If any requested node is not present.
1052+
*/
1053+
void remove_nodes_from(const std::vector<NodeID>& nodes) {
1054+
std::vector<NodeID> unique_nodes;
1055+
std::set<NodeID> seen;
1056+
for (const auto& node : nodes) {
1057+
if (seen.insert(node).second) {
1058+
unique_nodes.push_back(node);
1059+
}
1060+
}
1061+
1062+
for (const auto& node : unique_nodes) {
1063+
if (!has_node(node)) {
1064+
throw std::runtime_error("Node lookup failed: node not found.");
1065+
}
1066+
}
1067+
1068+
if (unique_nodes.empty()) {
1069+
return;
1070+
}
1071+
1072+
invalidate_min_cost_flow_state();
1073+
for (const auto& node : unique_nodes) {
1074+
auto vertex = find_vertex_by_id(node);
1075+
if (!vertex.has_value()) {
1076+
continue;
1077+
}
1078+
1079+
erase_incident_edge_properties(*vertex);
1080+
boost::clear_vertex(*vertex, g);
1081+
boost::remove_vertex(*vertex, g);
1082+
node_properties.erase(node);
1083+
}
1084+
1085+
rebuild_vertex_maps();
1086+
}
1087+
10281088
/**
10291089
* @brief Returns the outgoing neighbors of a node.
10301090
*

tests/test_remove_node.cpp

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,47 @@ void test_remove_node_in_multigraph_cleans_incident_edge_ids_only() {
107107
"remaining multigraph edge ids should still resolve to the right endpoints");
108108
}
109109

110+
void test_remove_nodes_from_removes_batch_and_ignores_duplicates() {
111+
nxpp::DiGraph graph;
112+
graph.add_edge("A", "B", 1.0, {{"label", "ab"}});
113+
graph.add_edge("B", "C", 2.0, {{"label", "bc"}});
114+
graph.add_edge("C", "D", 3.0, {{"label", "cd"}});
115+
graph.node("B")["kind"] = "middle";
116+
117+
graph.remove_nodes_from({"B", "C", "B"});
118+
119+
expect(!graph.has_node("B") && !graph.has_node("C"), "batch removal should remove each requested node once");
120+
expect(graph.has_node("A") && graph.has_node("D"), "batch removal should keep non-requested nodes");
121+
expect(graph.edges().empty(), "batch removal should remove incident edges for all requested nodes");
122+
expect(!graph.has_node_attr("B", "kind"), "batch removal should clean node attributes");
123+
expect(!graph.has_edge_attr("A", "B", "label"), "batch removal should clean incident edge attributes");
124+
expect(!graph.has_edge_attr("C", "D", "label"), "batch removal should clean incident edge attributes");
125+
}
126+
127+
void test_remove_nodes_from_missing_node_throws_without_mutating() {
128+
nxpp::DiGraph graph;
129+
graph.add_edge("A", "B", 1.0);
130+
graph.add_edge("B", "C", 2.0);
131+
132+
expect_runtime_error_message(
133+
[&graph] { graph.remove_nodes_from({"B", "missing"}); },
134+
"Node lookup failed: node not found.",
135+
"remove_nodes_from should reject any missing node"
136+
);
137+
138+
expect(graph.has_node("A") && graph.has_node("B") && graph.has_node("C"),
139+
"failed batch removal should leave nodes unchanged");
140+
expect(graph.edges().size() == 2, "failed batch removal should leave edges unchanged");
141+
}
142+
110143
int main() {
111144
return run_tests({
112145
{"remove middle node updates nodes and edges", test_remove_middle_node_updates_nodes_and_edges},
113146
{"remove_node cleans node and incident edge attributes", test_remove_node_cleans_node_and_incident_edge_attributes},
114147
{"algorithms still work after descriptor remap", test_algorithms_still_work_after_descriptor_remap},
115148
{"remove_node updates component views", test_remove_node_updates_component_views},
116149
{"remove_node in multigraph cleans incident edge ids only", test_remove_node_in_multigraph_cleans_incident_edge_ids_only},
150+
{"remove_nodes_from removes batch and ignores duplicates", test_remove_nodes_from_removes_batch_and_ignores_duplicates},
151+
{"remove_nodes_from missing node throws without mutating", test_remove_nodes_from_missing_node_throws_without_mutating},
117152
});
118153
}

0 commit comments

Comments
 (0)