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 8 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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ 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

Expand Down
154 changes: 154 additions & 0 deletions pydatastructs/graphs/_backend/cpp/Algorithms.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
#include <Python.h>
#include <unordered_map>
#include <queue>
#include <string>
#include "AdjacencyList.hpp"

static inline AdjacencyListGraphNode* get_node(AdjacencyListGraph* graph, const std::string& name) {
auto it = graph->node_map.find(name);
return (it != graph->node_map.end()) ? it->second : nullptr;
}

static PyObject* bfs_adjacency_list(PyObject* self, PyObject* args, PyObject* kwds) {
static char* kwlist[] = {(char*)"graph", (char*)"source_node", (char*)"operation", (char*)"extra_arg", NULL};

PyObject* py_graph = nullptr;
const char* source_node = nullptr;
PyObject* operation = nullptr;
PyObject* extra_arg = nullptr;

fprintf(stderr, "[bfs] Parsing arguments...\n");

if (!PyArg_ParseTupleAndKeywords(args, kwds, "OsO|O", kwlist,
&py_graph, &source_node, &operation, &extra_arg)) {
fprintf(stderr, "[bfs] Failed to parse arguments\n");
return NULL;
}

fprintf(stderr, "[bfs] Arguments parsed:\n");
fprintf(stderr, " - source_node: %s\n", source_node);
fprintf(stderr, " - extra_arg: %s\n", (extra_arg ? Py_TYPE(extra_arg)->tp_name : "NULL"));
fprintf(stderr, "[bfs] Checking type of py_graph...\n");
fprintf(stderr, " - Expected: %s\n", AdjacencyListGraphType.tp_name);
fprintf(stderr, " - Actual: %s\n", Py_TYPE(py_graph)->tp_name);
fprintf(stderr, " - Expected address: %p\n", &AdjacencyListGraphType);
fprintf(stderr, " - Actual type addr: %p\n", (void*)Py_TYPE(py_graph));

fprintf(stderr, "[bfs] Attempting to import _graph...\n");
PyObject* graph_module = PyImport_ImportModule("_graph");
if (!graph_module) {
PyErr_Print();
PyErr_SetString(PyExc_ImportError, "Could not import _graph module");
return NULL;
}

PyObject* expected_type = PyObject_GetAttrString(graph_module, "AdjacencyListGraph");
Py_DECREF(graph_module);

if (!expected_type || !PyType_Check(expected_type)) {
Py_XDECREF(expected_type);
PyErr_SetString(PyExc_TypeError, "Could not retrieve AdjacencyListGraph type");
return NULL;
}

if (!PyObject_IsInstance(py_graph, expected_type)) {
Py_DECREF(expected_type);
PyErr_SetString(PyExc_TypeError, "Expected an AdjacencyListGraph instance");
return NULL;
}

if (!PyCallable_Check(operation)) {
PyErr_SetString(PyExc_TypeError, "Expected a callable for operation");
fprintf(stderr, "[bfs] operation is not callable\n");
return NULL;
}

AdjacencyListGraph* graph = (AdjacencyListGraph*)py_graph;

if (!get_node(graph, source_node)) {
PyErr_SetString(PyExc_ValueError, "Source node does not exist in the graph");
fprintf(stderr, "[bfs] source_node not found in graph\n");
return NULL;
}

fprintf(stderr, "[bfs] Starting BFS from node: %s\n", source_node);

std::unordered_map<std::string, bool> visited;
std::queue<std::string> q;

q.push(source_node);
visited[source_node] = true;

while (!q.empty()) {
std::string curr = q.front();
q.pop();

fprintf(stderr, "[bfs] Visiting node: %s\n", curr.c_str());

auto* curr_node = get_node(graph, curr);
if (!curr_node) {
fprintf(stderr, "[bfs] Warning: node %s not found in node_map\n", curr.c_str());
continue;
}

const auto& neighbors = curr_node->adjacent;

if (!neighbors.empty()) {
for (const auto& [next_name, _] : neighbors) {
if (!visited[next_name]) {
fprintf(stderr, "[bfs] Considering neighbor: %s\n", next_name.c_str());

PyObject* result = nullptr;

if (extra_arg)
result = PyObject_CallFunction(operation, "ssO", curr.c_str(), next_name.c_str(), extra_arg);
else
result = PyObject_CallFunction(operation, "ss", curr.c_str(), next_name.c_str());

if (!result) {
fprintf(stderr, "[bfs] PyObject_CallFunction failed on (%s, %s)\n", curr.c_str(), next_name.c_str());
PyErr_Print();
return NULL;
}

int keep_going = PyObject_IsTrue(result);
Py_DECREF(result);

if (!keep_going) {
fprintf(stderr, "[bfs] Operation requested to stop traversal at edge (%s -> %s)\n", curr.c_str(), next_name.c_str());
Py_RETURN_NONE;
}

visited[next_name] = true;
q.push(next_name);
}
}
} else {
fprintf(stderr, "[bfs] Leaf node reached: %s\n", curr.c_str());

PyObject* result = nullptr;

if (extra_arg)
result = PyObject_CallFunction(operation, "sO", curr.c_str(), extra_arg);
else
result = PyObject_CallFunction(operation, "s", curr.c_str());

if (!result) {
fprintf(stderr, "[bfs] PyObject_CallFunction failed at leaf node (%s)\n", curr.c_str());
PyErr_Print();
return NULL;
}

int keep_going = PyObject_IsTrue(result);
Py_DECREF(result);

if (!keep_going) {
fprintf(stderr, "[bfs] Operation requested to stop traversal at leaf node %s\n", curr.c_str());
Py_RETURN_NONE;
}
}
}

fprintf(stderr, "[bfs] BFS traversal complete\n");
Py_RETURN_NONE;
}
46 changes: 46 additions & 0 deletions pydatastructs/graphs/_backend/cpp/algorithms.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#include <Python.h>
#include "Algorithms.hpp"

static PyTypeObject* get_adjacency_list_graph_type() {
static PyTypeObject* cached_type = nullptr;

if (cached_type != nullptr) return cached_type;

PyObject* graph_mod = PyImport_ImportModule("pydatastructs.graphs._backend.cpp._graph");
if (!graph_mod) {
PyErr_SetString(PyExc_ImportError, "[algorithms] Failed to import _graph module");
return nullptr;
}

PyObject* type_obj = PyObject_GetAttrString(graph_mod, "AdjacencyListGraph");
Py_DECREF(graph_mod);

if (!type_obj || !PyType_Check(type_obj)) {
Py_XDECREF(type_obj);
PyErr_SetString(PyExc_TypeError, "[algorithms] AdjacencyListGraph is not a type object");
return nullptr;
}

cached_type = reinterpret_cast<PyTypeObject*>(type_obj);
return cached_type;
}

extern PyTypeObject* get_adjacency_list_graph_type();

static PyMethodDef AlgorithmsMethods[] = {
{"bfs_adjacency_list", (PyCFunction)bfs_adjacency_list, METH_VARARGS | METH_KEYWORDS, "Run BFS with callback"},
{NULL, NULL, 0, NULL}
};

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

PyMODINIT_FUNC PyInit__algorithms(void) {
PyObject* graph_mod = PyImport_ImportModule("pydatastructs.graphs._backend.cpp._graph");
if (!graph_mod) return nullptr;
Py_DECREF(graph_mod);

return PyModule_Create(&algorithms_module);
}
6 changes: 5 additions & 1 deletion pydatastructs/graphs/_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
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"])]
25 changes: 15 additions & 10 deletions pydatastructs/graphs/algorithms.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from pydatastructs.graphs.graph import Graph
from pydatastructs.linear_data_structures.algorithms import merge_sort_parallel
from pydatastructs import PriorityQueue
from pydatastructs.graphs._backend.cpp import _algorithms

__all__ = [
'breadth_first_search',
Expand Down Expand Up @@ -81,16 +82,20 @@ 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:
if (graph._impl == "adjacency_list"):
extra_arg = args[0] if args else None
return _algorithms.bfs_adjacency_list(graph,source_node, operation, extra_arg)

def _breadth_first_search_adjacency_list(
graph, source_node, operation, *args, **kwargs):
Expand Down
17 changes: 17 additions & 0 deletions pydatastructs/graphs/tests/test_algorithms.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
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.utils.misc_util import Backend

def test_breadth_first_search():

Expand Down Expand Up @@ -39,6 +42,18 @@ def bfs_tree(curr_node, next_node, parent):
breadth_first_search(G1, V1.name, bfs_tree, 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'):
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, V1.name, bfs_tree, parent, backend = Backend.CPP)



V4 = GraphNode(0)
V5 = GraphNode(1)
Expand Down Expand Up @@ -131,6 +146,8 @@ def bfs_tree(curr_node, next_node, parent):
_test_breadth_first_search_parallel("List")
_test_breadth_first_search_parallel("Matrix")

test_breadth_first_search()

def test_minimum_spanning_tree():

def _test_minimum_spanning_tree(func, ds, algorithm, *args):
Expand Down
Loading
Loading