Skip to content

Commit e520be3

Browse files
authored
Add C++ Backend for BFS and use std::variant for Graph Node Data (#684)
1 parent c00e4ea commit e520be3

File tree

14 files changed

+463
-66
lines changed

14 files changed

+463
-66
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,10 @@ jobs:
142142
python -m pip install -r docs/requirements.txt
143143
144144
- name: Build package
145+
env:
146+
MACOSX_DEPLOYMENT_TARGET: 11.0
145147
run: |
146148
CXXFLAGS="-std=c++17" python scripts/build/install.py
147-
148149
- name: Run tests
149150
run: |
150151
python -c "import pydatastructs; pydatastructs.test()"

pydatastructs/graphs/_backend/cpp/AdjacencyList.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ static PyMethodDef AdjacencyListGraph_methods[] = {
349349
};
350350

351351

352-
PyTypeObject AdjacencyListGraphType = {
352+
inline PyTypeObject AdjacencyListGraphType = {
353353
PyVarObject_HEAD_INIT(NULL, 0) // ob_base
354354
"_graph.AdjacencyListGraph", // tp_name
355355
sizeof(AdjacencyListGraph), // tp_basicsize

pydatastructs/graphs/_backend/cpp/AdjacencyMatrix.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ static PyMethodDef AdjacencyMatrixGraph_methods[] = {
236236
{NULL}
237237
};
238238

239-
PyTypeObject AdjacencyMatrixGraphType = {
239+
inline PyTypeObject AdjacencyMatrixGraphType = {
240240
PyVarObject_HEAD_INIT(NULL, 0) // ob_base
241241
"_graph.AdjacencyMatrixGraph", // tp_name
242242
sizeof(AdjacencyMatrixGraph), // tp_basicsize
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#include <Python.h>
2+
#include <unordered_map>
3+
#include <queue>
4+
#include <string>
5+
#include <unordered_set>
6+
#include "AdjacencyList.hpp"
7+
#include "AdjacencyMatrix.hpp"
8+
9+
10+
static PyObject* breadth_first_search_adjacency_list(PyObject* self, PyObject* args, PyObject* kwargs) {
11+
PyObject* graph_obj;
12+
const char* source_name;
13+
PyObject* operation;
14+
PyObject* varargs = nullptr;
15+
PyObject* kwargs_dict = nullptr;
16+
17+
static const char* kwlist[] = {"graph", "source_node", "operation", "args", "kwargs", nullptr};
18+
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!sO|OO", const_cast<char**>(kwlist),
19+
&AdjacencyListGraphType, &graph_obj,
20+
&source_name, &operation,
21+
&varargs, &kwargs_dict)) {
22+
return nullptr;
23+
}
24+
25+
AdjacencyListGraph* cpp_graph = reinterpret_cast<AdjacencyListGraph*>(graph_obj);
26+
27+
auto it = cpp_graph->node_map.find(source_name);
28+
AdjacencyListGraphNode* start_node = it->second;
29+
30+
std::unordered_set<std::string> visited;
31+
std::queue<AdjacencyListGraphNode*> q;
32+
33+
q.push(start_node);
34+
visited.insert(start_node->name);
35+
36+
while (!q.empty()) {
37+
AdjacencyListGraphNode* node = q.front();
38+
q.pop();
39+
40+
for (const auto& [adj_name, adj_obj] : node->adjacent) {
41+
if (visited.count(adj_name)) continue;
42+
if (!PyObject_IsInstance(adj_obj, (PyObject*)&AdjacencyListGraphNodeType)) continue;
43+
44+
AdjacencyListGraphNode* adj_node = reinterpret_cast<AdjacencyListGraphNode*>(adj_obj);
45+
46+
PyObject* base_args = PyTuple_Pack(2,
47+
reinterpret_cast<PyObject*>(node),
48+
reinterpret_cast<PyObject*>(adj_node));
49+
if (!base_args)
50+
return nullptr;
51+
52+
PyObject* final_args;
53+
if (varargs && PyTuple_Check(varargs)) {
54+
final_args = PySequence_Concat(base_args, varargs);
55+
Py_DECREF(base_args);
56+
if (!final_args)
57+
return nullptr;
58+
} else {
59+
final_args = base_args;
60+
}
61+
62+
PyObject* result = PyObject_Call(operation, final_args, kwargs_dict);
63+
Py_DECREF(final_args);
64+
65+
if (!result)
66+
return nullptr;
67+
68+
Py_DECREF(result);
69+
70+
visited.insert(adj_name);
71+
q.push(adj_node);
72+
}
73+
}
74+
if (PyErr_Occurred()) {
75+
return nullptr;
76+
}
77+
78+
Py_RETURN_NONE;
79+
}
80+
81+
static PyObject* breadth_first_search_adjacency_matrix(PyObject* self, PyObject* args, PyObject* kwargs) {
82+
PyObject* graph_obj;
83+
const char* source_name;
84+
PyObject* operation;
85+
PyObject* varargs = nullptr;
86+
PyObject* kwargs_dict = nullptr;
87+
88+
static const char* kwlist[] = {"graph", "source_node", "operation", "args", "kwargs", nullptr};
89+
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!sO|OO", const_cast<char**>(kwlist),
90+
&AdjacencyMatrixGraphType, &graph_obj,
91+
&source_name, &operation,
92+
&varargs, &kwargs_dict)) {
93+
return nullptr;
94+
}
95+
96+
AdjacencyMatrixGraph* cpp_graph = reinterpret_cast<AdjacencyMatrixGraph*>(graph_obj);
97+
98+
auto it = cpp_graph->node_map.find(source_name);
99+
if (it == cpp_graph->node_map.end()) {
100+
PyErr_SetString(PyExc_KeyError, "Source node not found in graph");
101+
return nullptr;
102+
}
103+
AdjacencyMatrixGraphNode* start_node = it->second;
104+
105+
std::unordered_set<std::string> visited;
106+
std::queue<AdjacencyMatrixGraphNode*> q;
107+
108+
q.push(start_node);
109+
visited.insert(source_name);
110+
111+
while (!q.empty()) {
112+
AdjacencyMatrixGraphNode* node = q.front();
113+
q.pop();
114+
115+
std::string node_name = reinterpret_cast<GraphNode*>(node)->name;
116+
auto& neighbors = cpp_graph->matrix[node_name];
117+
118+
for (const auto& [adj_name, connected] : neighbors) {
119+
if (!connected || visited.count(adj_name)) continue;
120+
121+
auto adj_it = cpp_graph->node_map.find(adj_name);
122+
if (adj_it == cpp_graph->node_map.end()) continue;
123+
124+
AdjacencyMatrixGraphNode* adj_node = adj_it->second;
125+
126+
PyObject* base_args = PyTuple_Pack(2,
127+
reinterpret_cast<PyObject*>(node),
128+
reinterpret_cast<PyObject*>(adj_node));
129+
if (!base_args) return nullptr;
130+
131+
PyObject* final_args;
132+
if (varargs && PyTuple_Check(varargs)) {
133+
final_args = PySequence_Concat(base_args, varargs);
134+
Py_DECREF(base_args);
135+
if (!final_args) return nullptr;
136+
} else {
137+
final_args = base_args;
138+
}
139+
140+
PyObject* result = PyObject_Call(operation, final_args, kwargs_dict);
141+
Py_DECREF(final_args);
142+
if (!result) return nullptr;
143+
Py_DECREF(result);
144+
145+
visited.insert(adj_name);
146+
q.push(adj_node);
147+
}
148+
}
149+
150+
if (PyErr_Occurred()) {
151+
return nullptr;
152+
}
153+
154+
Py_RETURN_NONE;
155+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#include <Python.h>
2+
#include "Algorithms.hpp"
3+
#include "AdjacencyList.hpp"
4+
#include "AdjacencyMatrix.hpp"
5+
6+
static PyMethodDef AlgorithmsMethods[] = {
7+
{"bfs_adjacency_list", (PyCFunction)breadth_first_search_adjacency_list, METH_VARARGS | METH_KEYWORDS, "Run BFS on adjacency list with callback"},
8+
{"bfs_adjacency_matrix", (PyCFunction)breadth_first_search_adjacency_matrix, METH_VARARGS | METH_KEYWORDS, "Run BFS on adjacency matrix with callback"},
9+
{NULL, NULL, 0, NULL}
10+
};
11+
12+
static struct PyModuleDef algorithms_module = {
13+
PyModuleDef_HEAD_INIT,
14+
"_algorithms", NULL, -1, AlgorithmsMethods
15+
};
16+
17+
PyMODINIT_FUNC PyInit__algorithms(void) {
18+
return PyModule_Create(&algorithms_module);
19+
}

pydatastructs/graphs/_backend/cpp/graph.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@
66
#include "AdjacencyMatrixGraphNode.hpp"
77
#include "graph_bindings.hpp"
88

9+
#ifdef __cplusplus
10+
extern "C" {
11+
#endif
912

13+
PyMODINIT_FUNC PyInit__graph(void);
14+
15+
#ifdef __cplusplus
16+
}
17+
#endif
1018

1119
static struct PyModuleDef graph_module = {
1220
PyModuleDef_HEAD_INIT,

pydatastructs/graphs/_extensions.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
graph = '.'.join([project, module, backend, cpp, '_graph'])
1313
graph_sources = ['/'.join([project, module, backend, cpp,
1414
'graph.cpp']),"pydatastructs/utils/_backend/cpp/graph_utils.cpp"]
15+
algorithms = '.'.join([project, module, backend, cpp, '_algorithms'])
16+
algorithms_sources = ['/'.join([project, module, backend, cpp,
17+
'algorithms.cpp']),"pydatastructs/utils/_backend/cpp/graph_utils.cpp"]
1518

1619
include_dir = os.path.abspath(os.path.join(project, 'utils', '_backend', 'cpp'))
1720

18-
extensions = [Extension(graph, sources=graph_sources,include_dirs=[include_dir])]
21+
extensions = [Extension(graph, sources=graph_sources,include_dirs=[include_dir], language="c++", extra_compile_args=["-std=c++17"]),
22+
Extension(algorithms, sources=algorithms_sources,include_dirs=[include_dir], language="c++", extra_compile_args=["-std=c++17"]),
23+
]

pydatastructs/graphs/algorithms.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -81,16 +81,24 @@ def breadth_first_search(
8181
>>> G.add_edge(V2.name, V3.name)
8282
>>> breadth_first_search(G, V1.name, f, V3.name)
8383
"""
84-
raise_if_backend_is_not_python(
85-
breadth_first_search, kwargs.get('backend', Backend.PYTHON))
86-
import pydatastructs.graphs.algorithms as algorithms
87-
func = "_breadth_first_search_" + graph._impl
88-
if not hasattr(algorithms, func):
89-
raise NotImplementedError(
90-
"Currently breadth first search isn't implemented for "
91-
"%s graphs."%(graph._impl))
92-
return getattr(algorithms, func)(
93-
graph, source_node, operation, *args, **kwargs)
84+
backend = kwargs.get('backend', Backend.PYTHON)
85+
if backend == Backend.PYTHON:
86+
import pydatastructs.graphs.algorithms as algorithms
87+
func = "_breadth_first_search_" + graph._impl
88+
if not hasattr(algorithms, func):
89+
raise NotImplementedError(
90+
"Currently breadth first search isn't implemented for "
91+
"%s graphs."%(graph._impl))
92+
return getattr(algorithms, func)(
93+
graph, source_node, operation, *args, **kwargs)
94+
else:
95+
from pydatastructs.graphs._backend.cpp._algorithms import bfs_adjacency_list, bfs_adjacency_matrix
96+
if (graph._impl == "adjacency_list"):
97+
extra_args = args if args else ()
98+
return bfs_adjacency_list(graph, source_node, operation, extra_args)
99+
if (graph._impl == "adjacency_matrix"):
100+
extra_args = args if args else ()
101+
return bfs_adjacency_matrix(graph, source_node, operation, extra_args)
94102

95103
def _breadth_first_search_adjacency_list(
96104
graph, source_node, operation, *args, **kwargs):

pydatastructs/graphs/tests/test_algorithms.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
depth_first_search, shortest_paths,all_pair_shortest_paths, topological_sort,
55
topological_sort_parallel, max_flow, find_bridges)
66
from pydatastructs.utils.raises_util import raises
7+
from pydatastructs.utils.misc_util import AdjacencyListGraphNode, AdjacencyMatrixGraphNode
8+
from pydatastructs.graphs._backend.cpp import _graph
9+
from pydatastructs.graphs._backend.cpp import _algorithms
10+
from pydatastructs.utils.misc_util import Backend
711

812
def test_breadth_first_search():
913

@@ -40,6 +44,32 @@ def bfs_tree(curr_node, next_node, parent):
4044
assert (parent[V3.name] == V1.name and parent[V2.name] == V1.name) or \
4145
(parent[V3.name] == V2.name and parent[V2.name] == V1.name)
4246

47+
if (ds=='List'):
48+
parent2 = {}
49+
V9 = AdjacencyListGraphNode("9",0,backend = Backend.CPP)
50+
V10 = AdjacencyListGraphNode("10",0,backend = Backend.CPP)
51+
V11 = AdjacencyListGraphNode("11",0,backend = Backend.CPP)
52+
G2 = Graph(V9, V10, V11,implementation = 'adjacency_list', backend = Backend.CPP)
53+
assert G2.num_vertices()==3
54+
G2.add_edge("9", "10")
55+
G2.add_edge("10", "11")
56+
breadth_first_search(G2, "9", bfs_tree, parent2, backend = Backend.CPP)
57+
assert parent2[V10] == V9
58+
assert parent2[V11] == V10
59+
60+
if (ds == 'Matrix'):
61+
parent3 = {}
62+
V12 = AdjacencyMatrixGraphNode("12", 0, backend = Backend.CPP)
63+
V13 = AdjacencyMatrixGraphNode("13", 0, backend = Backend.CPP)
64+
V14 = AdjacencyMatrixGraphNode("14", 0, backend = Backend.CPP)
65+
G3 = Graph(V12, V13, V14, implementation = 'adjacency_matrix', backend = Backend.CPP)
66+
assert G3.num_vertices() == 3
67+
G3.add_edge("12", "13")
68+
G3.add_edge("13", "14")
69+
breadth_first_search(G3, "12", bfs_tree, parent3, backend = Backend.CPP)
70+
assert parent3[V13] == V12
71+
assert parent3[V14] == V13
72+
4373
V4 = GraphNode(0)
4474
V5 = GraphNode(1)
4575
V6 = GraphNode(2)

0 commit comments

Comments
 (0)