Skip to content

Add C++ Backend for BFS and Use std::variant for Graph Node Data #684

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Jun 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,10 @@ jobs:
python -m pip install -r docs/requirements.txt

- name: Build package
env:
MACOSX_DEPLOYMENT_TARGET: 11.0
run: |
CXXFLAGS="-std=c++17" python scripts/build/install.py

- name: Run tests
run: |
python -c "import pydatastructs; pydatastructs.test()"
Expand Down
2 changes: 1 addition & 1 deletion pydatastructs/graphs/_backend/cpp/AdjacencyList.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ static PyMethodDef AdjacencyListGraph_methods[] = {
};


PyTypeObject AdjacencyListGraphType = {
inline PyTypeObject AdjacencyListGraphType = {
PyVarObject_HEAD_INIT(NULL, 0) // ob_base
"_graph.AdjacencyListGraph", // tp_name
sizeof(AdjacencyListGraph), // tp_basicsize
Expand Down
2 changes: 1 addition & 1 deletion pydatastructs/graphs/_backend/cpp/AdjacencyMatrix.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ static PyMethodDef AdjacencyMatrixGraph_methods[] = {
{NULL}
};

PyTypeObject AdjacencyMatrixGraphType = {
inline PyTypeObject AdjacencyMatrixGraphType = {
PyVarObject_HEAD_INIT(NULL, 0) // ob_base
"_graph.AdjacencyMatrixGraph", // tp_name
sizeof(AdjacencyMatrixGraph), // tp_basicsize
Expand Down
155 changes: 155 additions & 0 deletions pydatastructs/graphs/_backend/cpp/Algorithms.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
#include <Python.h>
#include <unordered_map>
#include <queue>
#include <string>
#include <unordered_set>
#include "AdjacencyList.hpp"
#include "AdjacencyMatrix.hpp"


static PyObject* breadth_first_search_adjacency_list(PyObject* self, PyObject* args, PyObject* kwargs) {
PyObject* graph_obj;
const char* source_name;
PyObject* operation;
PyObject* varargs = nullptr;
PyObject* kwargs_dict = nullptr;

static const char* kwlist[] = {"graph", "source_node", "operation", "args", "kwargs", nullptr};
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!sO|OO", const_cast<char**>(kwlist),
&AdjacencyListGraphType, &graph_obj,
&source_name, &operation,
&varargs, &kwargs_dict)) {
return nullptr;
}

AdjacencyListGraph* cpp_graph = reinterpret_cast<AdjacencyListGraph*>(graph_obj);

auto it = cpp_graph->node_map.find(source_name);
AdjacencyListGraphNode* start_node = it->second;

std::unordered_set<std::string> visited;
std::queue<AdjacencyListGraphNode*> q;

q.push(start_node);
visited.insert(start_node->name);

while (!q.empty()) {
AdjacencyListGraphNode* node = q.front();
q.pop();

for (const auto& [adj_name, adj_obj] : node->adjacent) {
if (visited.count(adj_name)) continue;
if (!PyObject_IsInstance(adj_obj, (PyObject*)&AdjacencyListGraphNodeType)) continue;

AdjacencyListGraphNode* adj_node = reinterpret_cast<AdjacencyListGraphNode*>(adj_obj);

PyObject* base_args = PyTuple_Pack(2,
reinterpret_cast<PyObject*>(node),
reinterpret_cast<PyObject*>(adj_node));
if (!base_args)
return nullptr;

PyObject* final_args;
if (varargs && PyTuple_Check(varargs)) {
final_args = PySequence_Concat(base_args, varargs);
Py_DECREF(base_args);
if (!final_args)
return nullptr;
} else {
final_args = base_args;
}

PyObject* result = PyObject_Call(operation, final_args, kwargs_dict);
Py_DECREF(final_args);

if (!result)
return nullptr;

Py_DECREF(result);

visited.insert(adj_name);
q.push(adj_node);
}
}
if (PyErr_Occurred()) {
return nullptr;
}

Py_RETURN_NONE;
}

static PyObject* breadth_first_search_adjacency_matrix(PyObject* self, PyObject* args, PyObject* kwargs) {
PyObject* graph_obj;
const char* source_name;
PyObject* operation;
PyObject* varargs = nullptr;
PyObject* kwargs_dict = nullptr;

static const char* kwlist[] = {"graph", "source_node", "operation", "args", "kwargs", nullptr};
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!sO|OO", const_cast<char**>(kwlist),
&AdjacencyMatrixGraphType, &graph_obj,
&source_name, &operation,
&varargs, &kwargs_dict)) {
return nullptr;
}

AdjacencyMatrixGraph* cpp_graph = reinterpret_cast<AdjacencyMatrixGraph*>(graph_obj);

auto it = cpp_graph->node_map.find(source_name);
if (it == cpp_graph->node_map.end()) {
PyErr_SetString(PyExc_KeyError, "Source node not found in graph");
return nullptr;
}
AdjacencyMatrixGraphNode* start_node = it->second;

std::unordered_set<std::string> visited;
std::queue<AdjacencyMatrixGraphNode*> q;

q.push(start_node);
visited.insert(source_name);

while (!q.empty()) {
AdjacencyMatrixGraphNode* node = q.front();
q.pop();

std::string node_name = reinterpret_cast<GraphNode*>(node)->name;
auto& neighbors = cpp_graph->matrix[node_name];

for (const auto& [adj_name, connected] : neighbors) {
if (!connected || visited.count(adj_name)) continue;

auto adj_it = cpp_graph->node_map.find(adj_name);
if (adj_it == cpp_graph->node_map.end()) continue;

AdjacencyMatrixGraphNode* adj_node = adj_it->second;

PyObject* base_args = PyTuple_Pack(2,
reinterpret_cast<PyObject*>(node),
reinterpret_cast<PyObject*>(adj_node));
if (!base_args) return nullptr;

PyObject* final_args;
if (varargs && PyTuple_Check(varargs)) {
final_args = PySequence_Concat(base_args, varargs);
Py_DECREF(base_args);
if (!final_args) return nullptr;
} else {
final_args = base_args;
}

PyObject* result = PyObject_Call(operation, final_args, kwargs_dict);
Py_DECREF(final_args);
if (!result) return nullptr;
Py_DECREF(result);

visited.insert(adj_name);
q.push(adj_node);
}
}

if (PyErr_Occurred()) {
return nullptr;
}

Py_RETURN_NONE;
}
19 changes: 19 additions & 0 deletions pydatastructs/graphs/_backend/cpp/algorithms.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#include <Python.h>
#include "Algorithms.hpp"
#include "AdjacencyList.hpp"
#include "AdjacencyMatrix.hpp"

static PyMethodDef AlgorithmsMethods[] = {
{"bfs_adjacency_list", (PyCFunction)breadth_first_search_adjacency_list, METH_VARARGS | METH_KEYWORDS, "Run BFS on adjacency list with callback"},
{"bfs_adjacency_matrix", (PyCFunction)breadth_first_search_adjacency_matrix, METH_VARARGS | METH_KEYWORDS, "Run BFS on adjacency matrix with callback"},
{NULL, NULL, 0, NULL}
};

static struct PyModuleDef algorithms_module = {
PyModuleDef_HEAD_INIT,
"_algorithms", NULL, -1, AlgorithmsMethods
};

PyMODINIT_FUNC PyInit__algorithms(void) {
return PyModule_Create(&algorithms_module);
}
8 changes: 8 additions & 0 deletions pydatastructs/graphs/_backend/cpp/graph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@
#include "AdjacencyMatrixGraphNode.hpp"
#include "graph_bindings.hpp"

#ifdef __cplusplus
extern "C" {
#endif

PyMODINIT_FUNC PyInit__graph(void);

#ifdef __cplusplus
}
#endif

static struct PyModuleDef graph_module = {
PyModuleDef_HEAD_INIT,
Expand Down
7 changes: 6 additions & 1 deletion pydatastructs/graphs/_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@
graph = '.'.join([project, module, backend, cpp, '_graph'])
graph_sources = ['/'.join([project, module, backend, cpp,
'graph.cpp']),"pydatastructs/utils/_backend/cpp/graph_utils.cpp"]
algorithms = '.'.join([project, module, backend, cpp, '_algorithms'])
algorithms_sources = ['/'.join([project, module, backend, cpp,
'algorithms.cpp']),"pydatastructs/utils/_backend/cpp/graph_utils.cpp"]

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

extensions = [Extension(graph, sources=graph_sources,include_dirs=[include_dir])]
extensions = [Extension(graph, sources=graph_sources,include_dirs=[include_dir], language="c++", extra_compile_args=["-std=c++17"]),
Extension(algorithms, sources=algorithms_sources,include_dirs=[include_dir], language="c++", extra_compile_args=["-std=c++17"]),
]
28 changes: 18 additions & 10 deletions pydatastructs/graphs/algorithms.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,24 @@ def breadth_first_search(
>>> G.add_edge(V2.name, V3.name)
>>> breadth_first_search(G, V1.name, f, V3.name)
"""
raise_if_backend_is_not_python(
breadth_first_search, kwargs.get('backend', Backend.PYTHON))
import pydatastructs.graphs.algorithms as algorithms
func = "_breadth_first_search_" + graph._impl
if not hasattr(algorithms, func):
raise NotImplementedError(
"Currently breadth first search isn't implemented for "
"%s graphs."%(graph._impl))
return getattr(algorithms, func)(
graph, source_node, operation, *args, **kwargs)
backend = kwargs.get('backend', Backend.PYTHON)
if backend == Backend.PYTHON:
import pydatastructs.graphs.algorithms as algorithms
func = "_breadth_first_search_" + graph._impl
if not hasattr(algorithms, func):
raise NotImplementedError(
"Currently breadth first search isn't implemented for "
"%s graphs."%(graph._impl))
return getattr(algorithms, func)(
graph, source_node, operation, *args, **kwargs)
else:
from pydatastructs.graphs._backend.cpp._algorithms import bfs_adjacency_list, bfs_adjacency_matrix
if (graph._impl == "adjacency_list"):
extra_args = args if args else ()
return bfs_adjacency_list(graph, source_node, operation, extra_args)
if (graph._impl == "adjacency_matrix"):
extra_args = args if args else ()
return bfs_adjacency_matrix(graph, source_node, operation, extra_args)

def _breadth_first_search_adjacency_list(
graph, source_node, operation, *args, **kwargs):
Expand Down
30 changes: 30 additions & 0 deletions pydatastructs/graphs/tests/test_algorithms.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
depth_first_search, shortest_paths,all_pair_shortest_paths, topological_sort,
topological_sort_parallel, max_flow, find_bridges)
from pydatastructs.utils.raises_util import raises
from pydatastructs.utils.misc_util import AdjacencyListGraphNode, AdjacencyMatrixGraphNode
from pydatastructs.graphs._backend.cpp import _graph
from pydatastructs.graphs._backend.cpp import _algorithms
from pydatastructs.utils.misc_util import Backend

def test_breadth_first_search():

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

if (ds=='List'):
parent2 = {}
V9 = AdjacencyListGraphNode("9",0,backend = Backend.CPP)
V10 = AdjacencyListGraphNode("10",0,backend = Backend.CPP)
V11 = AdjacencyListGraphNode("11",0,backend = Backend.CPP)
G2 = Graph(V9, V10, V11,implementation = 'adjacency_list', backend = Backend.CPP)
assert G2.num_vertices()==3
G2.add_edge("9", "10")
G2.add_edge("10", "11")
breadth_first_search(G2, "9", bfs_tree, parent2, backend = Backend.CPP)
assert parent2[V10] == V9
assert parent2[V11] == V10

if (ds == 'Matrix'):
parent3 = {}
V12 = AdjacencyMatrixGraphNode("12", 0, backend = Backend.CPP)
V13 = AdjacencyMatrixGraphNode("13", 0, backend = Backend.CPP)
V14 = AdjacencyMatrixGraphNode("14", 0, backend = Backend.CPP)
G3 = Graph(V12, V13, V14, implementation = 'adjacency_matrix', backend = Backend.CPP)
assert G3.num_vertices() == 3
G3.add_edge("12", "13")
G3.add_edge("13", "14")
breadth_first_search(G3, "12", bfs_tree, parent3, backend = Backend.CPP)
assert parent3[V13] == V12
assert parent3[V14] == V13

V4 = GraphNode(0)
V5 = GraphNode(1)
V6 = GraphNode(2)
Expand Down
Loading
Loading