diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b16910bea..664a7758e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,9 +31,9 @@ jobs: - name: Install requirements run: | - pip install -r requirements.txt - pip install -r docs/requirements.txt - + python -m pip install -r requirements.txt + python -m pip install -r docs/requirements.txt + - name: Install lcov run: | sudo apt-get update @@ -41,8 +41,8 @@ jobs: - name: Build package run: | - CXXFLAGS=--coverage CFLAGS=--coverage python scripts/build/install.py -# coverage tests + CXXFLAGS="-std=c++17 --coverage" CFLAGS="--coverage" python scripts/build/install.py + # coverage tests - name: Run tests run: | python -m pytest --doctest-modules --cov=./ --cov-report=xml -s @@ -50,7 +50,7 @@ jobs: - name: Capture Coverage Data with lcov run: | lcov --capture --directory . --output-file coverage.info --no-external - + - name: Generate HTML Coverage Report with genhtml run: | genhtml coverage.info --output-directory coverage_report @@ -97,12 +97,12 @@ jobs: - name: Install requirements run: | - pip install -r requirements.txt - pip install -r docs/requirements.txt + python -m pip install -r requirements.txt + python -m pip install -r docs/requirements.txt - name: Build package run: | - python scripts/build/install.py + CXXFLAGS="-std=c++17" python scripts/build/install.py - name: Run tests run: | @@ -138,12 +138,12 @@ jobs: - name: Install requirements run: | - pip install -r requirements.txt - pip install -r docs/requirements.txt + python -m pip install -r requirements.txt + python -m pip install -r docs/requirements.txt - name: Build package run: | - python scripts/build/install.py + CXXFLAGS="-std=c++17" python scripts/build/install.py - name: Run tests run: | @@ -186,10 +186,12 @@ jobs: - name: Install requirements run: | - pip install -r requirements.txt - pip install -r docs/requirements.txt + python -m pip install -r requirements.txt + python -m pip install -r docs/requirements.txt - name: Build package + env: + CL: "/std:c++17" run: | python scripts/build/install.py diff --git a/environment.yml b/environment.yml index ba0dc8c1b..2d2ce160d 100644 --- a/environment.yml +++ b/environment.yml @@ -5,6 +5,7 @@ channels: dependencies: - python=3.8 - pip + - pytest - pip: - codecov - pytest-cov diff --git a/pydatastructs/graphs/__init__.py b/pydatastructs/graphs/__init__.py index 21e0a5f35..c1a70574a 100644 --- a/pydatastructs/graphs/__init__.py +++ b/pydatastructs/graphs/__init__.py @@ -9,6 +9,7 @@ from . import algorithms from . import adjacency_list from . import adjacency_matrix +from . import _extensions from .algorithms import ( breadth_first_search, diff --git a/pydatastructs/graphs/_backend/__init__.py b/pydatastructs/graphs/_backend/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pydatastructs/graphs/_backend/cpp/AdjacencyList.hpp b/pydatastructs/graphs/_backend/cpp/AdjacencyList.hpp new file mode 100644 index 000000000..5a1c3260b --- /dev/null +++ b/pydatastructs/graphs/_backend/cpp/AdjacencyList.hpp @@ -0,0 +1,393 @@ +#ifndef ADJACENCY_LIST_GRAPH_HPP +#define ADJACENCY_LIST_GRAPH_HPP + +#define PY_SSIZE_T_CLEAN +#include +#include +#include +#include +#include +#include "AdjacencyListGraphNode.hpp" +#include "GraphEdge.hpp" + +extern PyTypeObject AdjacencyListGraphType; + +typedef struct { + + PyObject_HEAD + PyObject* dict; + std::vector nodes; + std::unordered_map edges; + std::unordered_map node_map; + +} AdjacencyListGraph; + +static void AdjacencyListGraph_dealloc(AdjacencyListGraph* self) { + for (AdjacencyListGraphNode* node : self->nodes) { + Py_XDECREF(node); + } + self->nodes.clear(); + + for (auto& pair : self->edges) { + Py_XDECREF(pair.second); + } + self->edges.clear(); + + self->node_map.clear(); + + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +static PyObject* AdjacencyListGraph_new(PyTypeObject* type, PyObject* args, PyObject* kwds) { + AdjacencyListGraph* self = reinterpret_cast(type->tp_alloc(type, 0)); + if (!self) + return NULL; + + new (&self->nodes) std::vector(); + new (&self->edges) std::unordered_map(); + new (&self->node_map) std::unordered_map(); + + Py_ssize_t num_args = PyTuple_Size(args); + for (Py_ssize_t i = 0; i < num_args; ++i) { + PyObject* node_obj = PyTuple_GetItem(args, i); + if (!PyObject_IsInstance(node_obj, (PyObject*)&AdjacencyListGraphNodeType)) { + PyErr_SetString(PyExc_TypeError, "All arguments must be AdjacencyListGraphNode instances"); + + self->nodes.~vector(); + self->edges.~unordered_map(); + self->node_map.~unordered_map(); + Py_TYPE(self)->tp_free(reinterpret_cast(self)); + return NULL; + } + + AdjacencyListGraphNode* node = reinterpret_cast(node_obj); + + if (self->node_map.find(node->name) != self->node_map.end()) { + PyErr_Format(PyExc_ValueError, "Duplicate node with name '%s'", node->name.c_str()); + + self->nodes.~vector(); + self->edges.~unordered_map(); + self->node_map.~unordered_map(); + Py_TYPE(self)->tp_free(reinterpret_cast(self)); + return NULL; + } + + Py_INCREF(node); + self->nodes.push_back(node); + self->node_map[node->name] = node; + } + PyObject* impl_str = PyUnicode_FromString("adjacency_list"); + + if (PyObject_SetAttrString(reinterpret_cast(self), "_impl", impl_str) < 0) { + Py_DECREF(impl_str); + PyErr_SetString(PyExc_RuntimeError, "Failed to set _impl attribute"); + return NULL; + } + + Py_DECREF(impl_str); + return reinterpret_cast(self); +} + +static std::string make_edge_key(const std::string& source, const std::string& target) { + return source + "_" + target; +} + +static PyObject* AdjacencyListGraph_add_vertex(AdjacencyListGraph* self, PyObject* args) { + PyObject* node_obj; + + if (!PyArg_ParseTuple(args, "O", &node_obj)) { + PyErr_SetString(PyExc_ValueError, "Expected a single AdjacencyListGraphNode object"); + return NULL; + } + + if (!PyObject_IsInstance(node_obj, (PyObject*)&AdjacencyListGraphNodeType)) { + PyErr_SetString(PyExc_TypeError, "Object is not an AdjacencyListGraphNode"); + return NULL; + } + + AdjacencyListGraphNode* node = reinterpret_cast(node_obj); + + if (self->node_map.find(node->name) != self->node_map.end()) { + PyErr_SetString(PyExc_ValueError, "Node with this name already exists"); + return NULL; + } + + Py_INCREF(node); + self->nodes.push_back(node); + self->node_map[node->name] = node; + + Py_RETURN_NONE; +} + + +static PyObject* AdjacencyListGraph_is_adjacent(AdjacencyListGraph* self, PyObject* args) { + const char* node1_name_c; + const char* node2_name_c; + if (!PyArg_ParseTuple(args, "ss", &node1_name_c, &node2_name_c)) + return NULL; + + std::string node1_name(node1_name_c); + std::string node2_name(node2_name_c); + + auto it1 = self->node_map.find(node1_name); + if (it1 == self->node_map.end()) { + PyErr_SetString(PyExc_KeyError, "node1 not found"); + return NULL; + } + AdjacencyListGraphNode* node1 = it1->second; + + if (node1->adjacent.find(node2_name) != node1->adjacent.end()) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } +} + +static PyObject* AdjacencyListGraph_num_vertices(AdjacencyListGraph* self, PyObject* Py_UNUSED(ignored)) { + return PyLong_FromSize_t(self->nodes.size()); +} + +static PyObject* AdjacencyListGraph_num_edges(AdjacencyListGraph* self, PyObject* Py_UNUSED(ignored)) { + return PyLong_FromSize_t(self->edges.size()); +} + +static PyObject* AdjacencyListGraph_neighbors(AdjacencyListGraph* self, PyObject* args) { + const char* node_name_c; + if (!PyArg_ParseTuple(args, "s", &node_name_c)) + return NULL; + + std::string node_name(node_name_c); + auto it = self->node_map.find(node_name); + if (it == self->node_map.end()) { + PyErr_SetString(PyExc_KeyError, "Node not found"); + return NULL; + } + AdjacencyListGraphNode* node = it->second; + + PyObject* neighbors_list = PyList_New(0); + if (!neighbors_list) return NULL; + + for (const auto& adj_pair : node->adjacent) { + Py_INCREF(adj_pair.second); + PyList_Append(neighbors_list, (PyObject*)adj_pair.second); + } + + return neighbors_list; +} + +static PyObject* AdjacencyListGraph_remove_vertex(AdjacencyListGraph* self, PyObject* args) { + const char* name_c; + if (!PyArg_ParseTuple(args, "s", &name_c)) + return NULL; + + std::string name(name_c); + auto it = self->node_map.find(name); + if (it == self->node_map.end()) { + PyErr_SetString(PyExc_KeyError, "Node not found"); + return NULL; + } + + AdjacencyListGraphNode* node_to_remove = it->second; + + auto& nodes_vec = self->nodes; + nodes_vec.erase(std::remove(nodes_vec.begin(), nodes_vec.end(), node_to_remove), nodes_vec.end()); + + self->node_map.erase(it); + + Py_XDECREF(node_to_remove); + + for (auto& node_pair : self->node_map) { + AdjacencyListGraphNode* node = node_pair.second; + auto adj_it = node->adjacent.find(name); + if (adj_it != node->adjacent.end()) { + Py_XDECREF(adj_it->second); + node->adjacent.erase(adj_it); + } + } + + for (auto it = self->edges.begin(); it != self->edges.end(); ) { + const std::string& key = it->first; + + bool involves_node = false; + size_t underscore = key.find('_'); + if (underscore != std::string::npos) { + std::string source = key.substr(0, underscore); + std::string target = key.substr(underscore + 1); + if (source == name || target == name) + involves_node = true; + } + + if (involves_node) { + Py_XDECREF(it->second); + it = self->edges.erase(it); + } else { + ++it; + } + } + + Py_RETURN_NONE; +} + + +static PyObject* AdjacencyListGraph_get_edge(AdjacencyListGraph* self, PyObject* args) { + const char* source_c; + const char* target_c; + if (!PyArg_ParseTuple(args, "ss", &source_c, &target_c)) + return NULL; + + std::string key = make_edge_key(source_c, target_c); + auto it = self->edges.find(key); + if (it != self->edges.end()) { + Py_INCREF(it->second); + return reinterpret_cast(it->second); + } + + Py_RETURN_NONE; +} + +static PyObject* AdjacencyListGraph_remove_edge(AdjacencyListGraph* self, PyObject* args) { + const char* source_c; + const char* target_c; + if (!PyArg_ParseTuple(args, "ss", &source_c, &target_c)) + return NULL; + + std::string source(source_c); + std::string target(target_c); + + auto it_source = self->node_map.find(source); + auto it_target = self->node_map.find(target); + if (it_source == self->node_map.end() || it_target == self->node_map.end()) { + PyErr_SetString(PyExc_KeyError, "Source or target node not found"); + return NULL; + } + + AdjacencyListGraphNode* source_node = it_source->second; + + auto adj_it = source_node->adjacent.find(target); + if (adj_it != source_node->adjacent.end()) { + Py_XDECREF(adj_it->second); + source_node->adjacent.erase(adj_it); + } + + std::string key = make_edge_key(source, target); + auto edge_it = self->edges.find(key); + if (edge_it != self->edges.end()) { + Py_XDECREF(edge_it->second); + self->edges.erase(edge_it); + } + + Py_RETURN_NONE; +} + +static PyObject* AdjacencyListGraph_add_edge(AdjacencyListGraph* self, PyObject* args) { + const char* source_cstr; + const char* target_cstr; + PyObject* value = Py_None; + + if (!PyArg_ParseTuple(args, "ss|O", &source_cstr, &target_cstr, &value)) { + PyErr_SetString(PyExc_ValueError, "Expected source (str), target (str), and optional weight"); + return NULL; + } + + std::string source(source_cstr); + std::string target(target_cstr); + + if (self->node_map.find(source) == self->node_map.end()) { + PyErr_SetString(PyExc_ValueError, "Source node does not exist"); + return NULL; + } + if (self->node_map.find(target) == self->node_map.end()) { + PyErr_SetString(PyExc_ValueError, "Target node does not exist"); + return NULL; + } + + AdjacencyListGraphNode* source_node = self->node_map[source]; + AdjacencyListGraphNode* target_node = self->node_map[target]; + + PyObject* edge_args = PyTuple_Pack(3, reinterpret_cast(source_node), reinterpret_cast(target_node), value); + if (!edge_args) return NULL; + + PyObject* edge_obj = PyObject_CallObject(reinterpret_cast(&GraphEdgeType), edge_args); + Py_DECREF(edge_args); + + if (!edge_obj) + return NULL; + + std::string key = make_edge_key(source, target); + + auto it = self->edges.find(key); + if (it != self->edges.end()) { + Py_XDECREF(it->second); + it->second = reinterpret_cast(edge_obj); + } else { + self->edges[key] = reinterpret_cast(edge_obj); + } + + auto adj_it = source_node->adjacent.find(target); + if (adj_it == source_node->adjacent.end()) { + Py_INCREF(target_node); + source_node->adjacent[target] = reinterpret_cast(target_node); + } + + Py_RETURN_NONE; +} + + + + +static PyMethodDef AdjacencyListGraph_methods[] = { + {"add_vertex", (PyCFunction)AdjacencyListGraph_add_vertex, METH_VARARGS, "Add a vertex to the graph"}, + {"add_edge", (PyCFunction)AdjacencyListGraph_add_edge, METH_VARARGS, "Add an edge to the graph"}, + {"is_adjacent", (PyCFunction)AdjacencyListGraph_is_adjacent, METH_VARARGS, "Check adjacency between two nodes"}, + {"num_vertices", (PyCFunction)AdjacencyListGraph_num_vertices, METH_NOARGS, "Number of vertices"}, + {"num_edges", (PyCFunction)AdjacencyListGraph_num_edges, METH_NOARGS, "Number of edges"}, + {"neighbors", (PyCFunction)AdjacencyListGraph_neighbors, METH_VARARGS, "Get neighbors of a node"}, + {"remove_vertex", (PyCFunction)AdjacencyListGraph_remove_vertex, METH_VARARGS, "Remove a vertex"}, + {"get_edge", (PyCFunction)AdjacencyListGraph_get_edge, METH_VARARGS, "Get edge between source and target"}, + {"remove_edge", (PyCFunction)AdjacencyListGraph_remove_edge, METH_VARARGS, "Remove edge between source and target"}, + {NULL} +}; + + +PyTypeObject AdjacencyListGraphType = { + PyVarObject_HEAD_INIT(NULL, 0) // ob_base + "_graph.AdjacencyListGraph", // tp_name + sizeof(AdjacencyListGraph), // tp_basicsize + 0, // tp_itemsize + (destructor)AdjacencyListGraph_dealloc, // tp_dealloc + 0, // tp_vectorcall_offset or tp_print (depends on Python version) + 0, // tp_getattr + 0, // tp_setattr + 0, // tp_as_async / tp_reserved + 0, // tp_repr + 0, // tp_as_number + 0, // tp_as_sequence + 0, // tp_as_mapping + 0, // tp_hash + 0, // tp_call + 0, // tp_str + 0, // tp_getattro + 0, // tp_setattro + 0, // tp_as_buffer + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, // tp_flags + "Adjacency List Graph", // tp_doc + 0, // tp_traverse + 0, // tp_clear + 0, // tp_richcompare + 0, // tp_weaklistoffset + 0, // tp_iter + 0, // tp_iternext + AdjacencyListGraph_methods, // tp_methods + 0, // tp_members + 0, // tp_getset + 0, // tp_base + 0, // tp_dict + 0, // tp_descr_get + 0, // tp_descr_set + offsetof(AdjacencyListGraph, dict), // tp_dictoffset + 0, // tp_init + 0, // tp_alloc + AdjacencyListGraph_new // tp_new +}; + +#endif diff --git a/pydatastructs/graphs/_backend/cpp/AdjacencyMatrix.hpp b/pydatastructs/graphs/_backend/cpp/AdjacencyMatrix.hpp new file mode 100644 index 000000000..0222f0b88 --- /dev/null +++ b/pydatastructs/graphs/_backend/cpp/AdjacencyMatrix.hpp @@ -0,0 +1,280 @@ +#ifndef ADJACENCY_MATRIX_GRAPH_HPP +#define ADJACENCY_MATRIX_GRAPH_HPP + +#define PY_SSIZE_T_CLEAN +#include +#include +#include +#include +#include "AdjacencyMatrixGraphNode.hpp" +#include "GraphEdge.hpp" +#include "GraphNode.hpp" + +extern PyTypeObject AdjacencyMatrixGraphNodeType; + +typedef struct { + PyObject_HEAD + PyObject* dict; + std::vector nodes; + std::unordered_map > matrix; + std::unordered_map node_map; + std::unordered_map edge_weights; +} AdjacencyMatrixGraph; + +static void AdjacencyMatrixGraph_dealloc(AdjacencyMatrixGraph* self) +{ + for (auto& pair : self->edge_weights) { + Py_XDECREF(pair.second); + } + self->edge_weights.clear(); + + for (AdjacencyMatrixGraphNode* node : self->nodes) { + Py_XDECREF(node); + } + self->nodes.clear(); + + self->node_map.clear(); + self->matrix.clear(); + + Py_XDECREF(self->dict); + + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +static PyObject* AdjacencyMatrixGraph_new(PyTypeObject* type, PyObject* args, PyObject* kwds) +{ + AdjacencyMatrixGraph* self; + self = reinterpret_cast(type->tp_alloc(type, 0)); + if (self != NULL) { + new (&self->nodes) std::vector(); + new (&self->node_map) std::unordered_map(); + new (&self->matrix) std::unordered_map >(); + new (&self->edge_weights) std::unordered_map(); + + PyObject* vertices; + if (!PyArg_ParseTuple(args, "O", &vertices)) { + Py_DECREF(self); + return NULL; + } + + if (!PyTuple_Check(vertices)) { + PyErr_SetString(PyExc_TypeError, "Expected a tuple of vertices"); + Py_DECREF(self); + return NULL; + } + + Py_ssize_t len = PyTuple_Size(vertices); + for (Py_ssize_t i = 0; i < len; ++i) { + PyObject* item = PyTuple_GetItem(vertices, i); + if (!PyObject_TypeCheck(item, &AdjacencyMatrixGraphNodeType)) { + PyErr_SetString(PyExc_TypeError, "All elements must be AdjacencyMatrixGraphNode instances"); + Py_DECREF(self); + return NULL; + } + Py_INCREF(item); + AdjacencyMatrixGraphNode* node = reinterpret_cast(item); + std::string name =(reinterpret_cast(node))->name; + self->nodes.push_back(node); + self->node_map[name] = node; + self->matrix[name] = std::unordered_map(); + } + + PyObject* impl_str = PyUnicode_FromString("adjacency_matrix"); + + if (PyObject_SetAttrString(reinterpret_cast(self), "_impl", impl_str) < 0) { + Py_DECREF(impl_str); + PyErr_SetString(PyExc_RuntimeError, "Failed to set _impl attribute"); + return NULL; + } + + Py_DECREF(impl_str); + } + return reinterpret_cast(self); +} + +static PyObject* AdjacencyMatrixGraph_add_edge(AdjacencyMatrixGraph* self, PyObject* args, PyObject* kwds) +{ + const char *source, *target; + PyObject *cost_obj = Py_None; + static char* kwlist[] = {"source", "target", "cost", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "ss|O", kwlist, &source, &target, &cost_obj)) + return NULL; + + std::string src(source); + std::string dst(target); + + if (self->matrix.find(src) == self->matrix.end() || + self->matrix.find(dst) == self->matrix.end()) { + PyErr_Format(PyExc_ValueError, "Vertex %s or %s not in graph", source, target); + return NULL; + } + + self->matrix[src][dst] = true; + + if (cost_obj != Py_None) { + double cost = PyFloat_AsDouble(cost_obj); + if (PyErr_Occurred()) return NULL; + + PyObject* cost_pyfloat = PyFloat_FromDouble(cost); + if (!cost_pyfloat) return NULL; + + PyObject* args = Py_BuildValue("OOO", reinterpret_cast(self->node_map[src]), reinterpret_cast(self->node_map[dst]), cost_pyfloat); + Py_DECREF(cost_pyfloat); + if (!args) return NULL; + + PyObject* edge_obj = PyObject_CallObject(reinterpret_cast(&GraphEdgeType), args); + Py_DECREF(args); + if (!edge_obj) return NULL; + + self->edge_weights[src + "_" + dst] = reinterpret_cast(edge_obj); + } + + Py_RETURN_NONE; +} + +static PyObject* AdjacencyMatrixGraph_remove_edge(AdjacencyMatrixGraph* self, PyObject* args) +{ + const char *source, *target; + if (!PyArg_ParseTuple(args, "ss", &source, &target)) + return NULL; + + std::string src(source); + std::string dst(target); + + if (self->matrix.find(src) != self->matrix.end()) { + self->matrix[src][dst] = false; + self->edge_weights.erase(src + "_" + dst); + } + + Py_RETURN_NONE; +} + +static PyObject* AdjacencyMatrixGraph_neighbors(AdjacencyMatrixGraph* self, PyObject* args) +{ + const char *node; + if (!PyArg_ParseTuple(args, "s", &node)) { + return NULL; + } + + std::string key(node); + if (self->matrix.find(key) == self->matrix.end()) { + PyErr_SetString(PyExc_KeyError, "Node not found"); + return NULL; + } + + PyObject* list = PyList_New(0); + for (const auto& pair : self->matrix[key]) { + if (pair.second) { + const std::string& neighbor_name = pair.first; + AdjacencyMatrixGraphNode* neighbor = self->node_map[neighbor_name]; + Py_INCREF(neighbor); + PyList_Append(list, (PyObject*)neighbor); + } + } + return list; +} + + +static PyObject* AdjacencyMatrixGraph_num_vertices(AdjacencyMatrixGraph* self, PyObject* Py_UNUSED(ignored)) +{ + return PyLong_FromSize_t(self->nodes.size()); +} + +static PyObject* AdjacencyMatrixGraph_num_edges(AdjacencyMatrixGraph* self, PyObject* Py_UNUSED(ignored)) +{ + size_t count = 0; + for (const auto& row : self->matrix) { + for (const auto& entry : row.second) { + if (entry.second) + count++; + } + } + return PyLong_FromSize_t(count); +} + +static PyObject* AdjacencyMatrixGraph_get_edge(AdjacencyMatrixGraph* self, PyObject* args) +{ + const char *source, *target; + if (!PyArg_ParseTuple(args, "ss", &source, &target)) + return NULL; + + std::string key = std::string(source) + "_" + std::string(target); + auto it = self->edge_weights.find(key); + if (it != self->edge_weights.end()) { + return reinterpret_cast(it->second); + } + Py_RETURN_NONE; +} + +static PyObject* AdjacencyMatrixGraph_is_adjacent(AdjacencyMatrixGraph* self, PyObject* args) +{ + const char *source, *target; + if (!PyArg_ParseTuple(args, "ss", &source, &target)) + return NULL; + + std::string src(source); + std::string dst(target); + if (self->matrix.find(src) == self->matrix.end()) + Py_RETURN_FALSE; + + auto& row = self->matrix[src]; + if (row.find(dst) != row.end() && row[dst]) + Py_RETURN_TRUE; + + Py_RETURN_FALSE; +} + +static PyMethodDef AdjacencyMatrixGraph_methods[] = { + {"add_edge", (PyCFunction)AdjacencyMatrixGraph_add_edge, METH_VARARGS | METH_KEYWORDS, "Add an edge between two nodes."}, + {"remove_edge", (PyCFunction)AdjacencyMatrixGraph_remove_edge, METH_VARARGS, "Remove an edge between two nodes."}, + {"neighbors", (PyCFunction)AdjacencyMatrixGraph_neighbors, METH_VARARGS, "Return neighbors of a node."}, + {"num_vertices", (PyCFunction)AdjacencyMatrixGraph_num_vertices, METH_NOARGS, "Return number of vertices."}, + {"num_edges", (PyCFunction)AdjacencyMatrixGraph_num_edges, METH_NOARGS, "Return number of edges."}, + {"get_edge", (PyCFunction)AdjacencyMatrixGraph_get_edge, METH_VARARGS, "Return the edge object between two nodes if exists."}, + {"is_adjacent", (PyCFunction)AdjacencyMatrixGraph_is_adjacent, METH_VARARGS, "Check if there is an edge between two nodes."}, + {NULL} +}; + +PyTypeObject AdjacencyMatrixGraphType = { + PyVarObject_HEAD_INIT(NULL, 0) // ob_base + "_graph.AdjacencyMatrixGraph", // tp_name + sizeof(AdjacencyMatrixGraph), // tp_basicsize + 0, // tp_itemsize + (destructor)AdjacencyMatrixGraph_dealloc, // tp_dealloc + 0, // tp_vectorcall_offset or tp_print (removed in Python 3.9) + 0, // tp_getattr + 0, // tp_setattr + 0, // tp_as_async / tp_reserved + 0, // tp_repr + 0, // tp_as_number + 0, // tp_as_sequence + 0, // tp_as_mapping + 0, // tp_hash + 0, // tp_call + 0, // tp_str + 0, // tp_getattro + 0, // tp_setattro + 0, // tp_as_buffer + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, // tp_flags + "Adjacency matrix graph", // tp_doc + 0, // tp_traverse + 0, // tp_clear + 0, // tp_richcompare + 0, // tp_weaklistoffset + 0, // tp_iter + 0, // tp_iternext + AdjacencyMatrixGraph_methods, // tp_methods + 0, // tp_members + 0, // tp_getset + 0, // tp_base + 0, // tp_dict + 0, // tp_descr_get + 0, // tp_descr_set + offsetof(AdjacencyMatrixGraph, dict), // tp_dictoffset + 0, // tp_init + 0, // tp_alloc + AdjacencyMatrixGraph_new // tp_new +}; + +#endif diff --git a/pydatastructs/graphs/_backend/cpp/__init__.py b/pydatastructs/graphs/_backend/cpp/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pydatastructs/graphs/_backend/cpp/graph.cpp b/pydatastructs/graphs/_backend/cpp/graph.cpp new file mode 100644 index 000000000..5d04b1d15 --- /dev/null +++ b/pydatastructs/graphs/_backend/cpp/graph.cpp @@ -0,0 +1,60 @@ +#define PY_SSIZE_T_CLEAN +#include +#include "AdjacencyList.hpp" +#include "AdjacencyMatrix.hpp" +#include "AdjacencyListGraphNode.hpp" +#include "AdjacencyMatrixGraphNode.hpp" +#include "graph_bindings.hpp" + + + +static struct PyModuleDef graph_module = { + PyModuleDef_HEAD_INIT, + "_graph", + "C++ module for graphs", + -1, + NULL, +}; + +PyMODINIT_FUNC PyInit__graph(void) { + PyObject* m; + + if (PyType_Ready(&AdjacencyListGraphType) < 0) + return NULL; + + if (PyType_Ready(&AdjacencyListGraphNodeType) < 0) + return NULL; + + if (PyType_Ready(&AdjacencyMatrixGraphType) < 0) + return NULL; + + if (PyType_Ready(&AdjacencyMatrixGraphNodeType) < 0) + return NULL; + + m = PyModule_Create(&graph_module); + if (m == NULL) + return NULL; + + Py_INCREF(&AdjacencyListGraphType); + if (PyModule_AddObject(m, "AdjacencyListGraph", (PyObject*)&AdjacencyListGraphType) < 0) { + Py_DECREF(&AdjacencyListGraphType); + Py_DECREF(m); + return NULL; + } + + Py_INCREF(&AdjacencyListGraphNodeType); + if (PyModule_AddObject(m, "AdjacencyListGraphNode", (PyObject*)&AdjacencyListGraphNodeType) < 0) { + Py_DECREF(&AdjacencyListGraphNodeType); + Py_DECREF(m); + return NULL; + } + + Py_INCREF(&AdjacencyMatrixGraphType); + if (PyModule_AddObject(m, "AdjacencyMatrixGraph", (PyObject*)&AdjacencyMatrixGraphType) < 0) { + Py_DECREF(&AdjacencyMatrixGraphType); + Py_DECREF(m); + return NULL; + } + + return m; +} diff --git a/pydatastructs/graphs/_extensions.py b/pydatastructs/graphs/_extensions.py new file mode 100644 index 000000000..b3ddcd6f1 --- /dev/null +++ b/pydatastructs/graphs/_extensions.py @@ -0,0 +1,18 @@ +from setuptools import Extension +import os + +project = 'pydatastructs' + +module = 'graphs' + +backend = '_backend' + +cpp = 'cpp' + +graph = '.'.join([project, module, backend, cpp, '_graph']) +graph_sources = ['/'.join([project, module, backend, cpp, + 'graph.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])] diff --git a/pydatastructs/graphs/adjacency_list.py b/pydatastructs/graphs/adjacency_list.py index cc9de94b6..bd901b380 100644 --- a/pydatastructs/graphs/adjacency_list.py +++ b/pydatastructs/graphs/adjacency_list.py @@ -1,4 +1,5 @@ from pydatastructs.graphs.graph import Graph +from pydatastructs.graphs._backend.cpp import _graph from pydatastructs.utils.misc_util import ( GraphEdge, Backend, raise_if_backend_is_not_python) @@ -16,14 +17,21 @@ class AdjacencyList(Graph): pydatastructs.graphs.graph.Graph """ def __new__(cls, *vertices, **kwargs): - raise_if_backend_is_not_python( - cls, kwargs.get('backend', Backend.PYTHON)) - obj = object.__new__(cls) - for vertex in vertices: - obj.__setattr__(vertex.name, vertex) - obj.vertices = [vertex.name for vertex in vertices] - obj.edge_weights = {} - return obj + + backend = kwargs.get('backend', Backend.PYTHON) + if backend == Backend.PYTHON: + obj = object.__new__(cls) + for vertex in vertices: + obj.__setattr__(vertex.name, vertex) + obj.vertices = [vertex.name for vertex in vertices] + obj.edge_weights = {} + obj._impl = 'adjacency_list' + return obj + else: + graph = _graph.AdjacencyListGraph() + for vertice in vertices: + graph.add_vertex(vertice) + return graph @classmethod def methods(self): diff --git a/pydatastructs/graphs/adjacency_matrix.py b/pydatastructs/graphs/adjacency_matrix.py index 48e0d3489..9c2326b86 100644 --- a/pydatastructs/graphs/adjacency_matrix.py +++ b/pydatastructs/graphs/adjacency_matrix.py @@ -1,4 +1,5 @@ from pydatastructs.graphs.graph import Graph +from pydatastructs.graphs._backend.cpp import _graph from pydatastructs.utils.misc_util import ( GraphEdge, raise_if_backend_is_not_python, Backend) @@ -17,17 +18,20 @@ class AdjacencyMatrix(Graph): pydatastructs.graphs.graph.Graph """ def __new__(cls, *vertices, **kwargs): - raise_if_backend_is_not_python( - cls, kwargs.get('backend', Backend.PYTHON)) - obj = object.__new__(cls) - obj.vertices = [vertex.name for vertex in vertices] - for vertex in vertices: - obj.__setattr__(vertex.name, vertex) - obj.matrix = {} - for vertex in vertices: - obj.matrix[vertex.name] = {} - obj.edge_weights = {} - return obj + backend = kwargs.get('backend', Backend.PYTHON) + if backend == Backend.PYTHON: + obj = object.__new__(cls) + obj.vertices = [vertex.name for vertex in vertices] + for vertex in vertices: + obj.__setattr__(vertex.name, vertex) + obj.matrix = {} + for vertex in vertices: + obj.matrix[vertex.name] = {} + obj.edge_weights = {} + obj._impl = 'adjacency_matrix' + return obj + else: + return _graph.AdjacencyMatrixGraph(vertices) @classmethod def methods(self): diff --git a/pydatastructs/graphs/graph.py b/pydatastructs/graphs/graph.py index 24f33a17b..39c2692e3 100644 --- a/pydatastructs/graphs/graph.py +++ b/pydatastructs/graphs/graph.py @@ -70,19 +70,19 @@ class Graph(object): __slots__ = ['_impl'] def __new__(cls, *args, **kwargs): - raise_if_backend_is_not_python( - cls, kwargs.get('backend', Backend.PYTHON)) - default_impl = args[0]._impl if args else 'adjacency_list' + backend = kwargs.get('backend', Backend.PYTHON) + try: + default_impl = args[0]._impl if args else 'adjacency_list' + except: + default_impl = 'adjacency_list' implementation = kwargs.get('implementation', default_impl) if implementation == 'adjacency_list': from pydatastructs.graphs.adjacency_list import AdjacencyList - obj = AdjacencyList(*args) - obj._impl = implementation + obj = AdjacencyList(*args, **kwargs) return obj elif implementation == 'adjacency_matrix': from pydatastructs.graphs.adjacency_matrix import AdjacencyMatrix - obj = AdjacencyMatrix(*args) - obj._impl = implementation + obj = AdjacencyMatrix(*args, **kwargs) return obj else: raise NotImplementedError("%s implementation is not a part " diff --git a/pydatastructs/graphs/tests/test_adjacency_list.py b/pydatastructs/graphs/tests/test_adjacency_list.py index 3dcef8a7a..6f82763ac 100644 --- a/pydatastructs/graphs/tests/test_adjacency_list.py +++ b/pydatastructs/graphs/tests/test_adjacency_list.py @@ -1,6 +1,7 @@ from pydatastructs.graphs import Graph from pydatastructs.utils import AdjacencyListGraphNode from pydatastructs.utils.raises_util import raises +from pydatastructs.utils.misc_util import Backend def test_adjacency_list(): v_1 = AdjacencyListGraphNode('v_1', 1) @@ -42,3 +43,41 @@ def test_adjacency_list(): assert raises(ValueError, lambda: g.add_edge('u', 'v')) assert raises(ValueError, lambda: g.add_edge('v', 'x')) + + v_4 = AdjacencyListGraphNode('v_4', 4, backend = Backend.CPP) + v_5 = AdjacencyListGraphNode('v_5', 5, backend = Backend.CPP) + g2 = Graph(v_4,v_5,implementation = 'adjacency_list', backend = Backend.CPP) + v_6 = AdjacencyListGraphNode('v_6', 6, backend = Backend.CPP) + assert raises(ValueError, lambda: g2.add_vertex(v_5)) + g2.add_vertex(v_6) + g2.add_edge('v_4', 'v_5') + g2.add_edge('v_5', 'v_6') + g2.add_edge('v_4', 'v_6') + assert g2.is_adjacent('v_4', 'v_5') is True + assert g2.is_adjacent('v_5', 'v_6') is True + assert g2.is_adjacent('v_4', 'v_6') is True + assert g2.is_adjacent('v_5', 'v_4') is False + assert g2.is_adjacent('v_6', 'v_5') is False + assert g2.is_adjacent('v_6', 'v_4') is False + assert g2.num_edges() == 3 + assert g2.num_vertices() == 3 + neighbors = g2.neighbors('v_4') + assert neighbors == [v_6, v_5] + v = AdjacencyListGraphNode('v', 4, backend = Backend.CPP) + g2.add_vertex(v) + g2.add_edge('v_4', 'v', 0) + g2.add_edge('v_5', 'v', 0) + g2.add_edge('v_6', 'v', 0) + assert g2.is_adjacent('v_4', 'v') is True + assert g2.is_adjacent('v_5', 'v') is True + assert g2.is_adjacent('v_6', 'v') is True + e1 = g2.get_edge('v_4', 'v') + e2 = g2.get_edge('v_5', 'v') + e3 = g2.get_edge('v_6', 'v') + assert (str(e1)) == "('v_4', 'v')" + assert (str(e2)) == "('v_5', 'v')" + assert (str(e3)) == "('v_6', 'v')" + g2.remove_edge('v_4', 'v') + assert g2.is_adjacent('v_4', 'v') is False + g2.remove_vertex('v') + assert raises(ValueError, lambda: g2.add_edge('v_4', 'v')) diff --git a/pydatastructs/graphs/tests/test_adjacency_matrix.py b/pydatastructs/graphs/tests/test_adjacency_matrix.py index 2dace4260..27dc81790 100644 --- a/pydatastructs/graphs/tests/test_adjacency_matrix.py +++ b/pydatastructs/graphs/tests/test_adjacency_matrix.py @@ -1,6 +1,7 @@ from pydatastructs.graphs import Graph from pydatastructs.utils import AdjacencyMatrixGraphNode from pydatastructs.utils.raises_util import raises +from pydatastructs.utils.misc_util import Backend def test_AdjacencyMatrix(): v_0 = AdjacencyMatrixGraphNode(0, 0) @@ -30,3 +31,23 @@ def test_AdjacencyMatrix(): assert raises(ValueError, lambda: g.add_edge('v', 'x')) assert raises(ValueError, lambda: g.add_edge(2, 3)) assert raises(ValueError, lambda: g.add_edge(3, 2)) + + v_3 = AdjacencyMatrixGraphNode('0', 0, backend = Backend.CPP) + v_4 = AdjacencyMatrixGraphNode('1', 1, backend = Backend.CPP) + v_5 = AdjacencyMatrixGraphNode('2', 2, backend = Backend.CPP) + g2 = Graph(v_3, v_4, v_5, implementation = 'adjacency_matrix', backend = Backend.CPP) + g2.add_edge('0', '1', 0) + g2.add_edge('1', '2', 0) + g2.add_edge('2', '0', 0) + assert g2.is_adjacent('0', '1') is True + assert g2.is_adjacent('1', '2') is True + assert g2.is_adjacent('2', '0') is True + assert g2.is_adjacent('1', '0') is False + assert g2.is_adjacent('2', '1') is False + assert g2.is_adjacent('0', '2') is False + neighbors = g2.neighbors('0') + assert neighbors == [v_4] + g2.remove_edge('0', '1') + assert g2.is_adjacent('0', '1') is False + assert raises(ValueError, lambda: g2.add_edge('u', 'v')) + assert raises(ValueError, lambda: g2.add_edge('v', 'x')) diff --git a/pydatastructs/utils/_backend/cpp/AdjacencyListGraphNode.hpp b/pydatastructs/utils/_backend/cpp/AdjacencyListGraphNode.hpp index 8aa42f66a..65e54e4fb 100644 --- a/pydatastructs/utils/_backend/cpp/AdjacencyListGraphNode.hpp +++ b/pydatastructs/utils/_backend/cpp/AdjacencyListGraphNode.hpp @@ -187,7 +187,7 @@ static PyMethodDef AdjacencyListGraphNode_methods[] = { {NULL} }; -PyTypeObject AdjacencyListGraphNodeType = { +inline PyTypeObject AdjacencyListGraphNodeType = { /* tp_name */ PyVarObject_HEAD_INIT(NULL, 0) "AdjacencyListGraphNode", /* tp_basicsize */ sizeof(AdjacencyListGraphNode), /* tp_itemsize */ 0, diff --git a/pydatastructs/utils/_backend/cpp/AdjacencyMatrixGraphNode.hpp b/pydatastructs/utils/_backend/cpp/AdjacencyMatrixGraphNode.hpp index 8c35754f6..adf8573c3 100644 --- a/pydatastructs/utils/_backend/cpp/AdjacencyMatrixGraphNode.hpp +++ b/pydatastructs/utils/_backend/cpp/AdjacencyMatrixGraphNode.hpp @@ -6,6 +6,8 @@ #include #include "GraphNode.hpp" +extern PyTypeObject AdjacencyMatrixGraphNodeType; + typedef struct { GraphNode super; } AdjacencyMatrixGraphNode; @@ -26,7 +28,7 @@ static PyObject* AdjacencyMatrixGraphNode_new(PyTypeObject* type, PyObject* args return reinterpret_cast(self); } -PyTypeObject AdjacencyMatrixGraphNodeType = { +inline PyTypeObject AdjacencyMatrixGraphNodeType = { /* tp_name */ PyVarObject_HEAD_INIT(NULL, 0) "AdjacencyMatrixGraphNode", /* tp_basicsize */ sizeof(AdjacencyMatrixGraphNode), /* tp_itemsize */ 0, diff --git a/pydatastructs/utils/_backend/cpp/GraphEdge.hpp b/pydatastructs/utils/_backend/cpp/GraphEdge.hpp index 1ef21008a..757bd89bd 100644 --- a/pydatastructs/utils/_backend/cpp/GraphEdge.hpp +++ b/pydatastructs/utils/_backend/cpp/GraphEdge.hpp @@ -49,22 +49,20 @@ static PyObject* GraphEdge_new(PyTypeObject* type, PyObject* args, PyObject* kwd } static PyObject* GraphEdge_str(GraphEdge* self) { - PyObject* source_name = PyObject_GetAttrString(self->source, "name"); - PyObject* target_name = PyObject_GetAttrString(self->target, "name"); + std::string source_name = (reinterpret_cast(self->source))->name; + std::string target_name = (reinterpret_cast(self->target))->name; - if (!source_name || !target_name) { + if (source_name.empty() || target_name.empty()) { PyErr_SetString(PyExc_AttributeError, "Both nodes must have a 'name' attribute."); return NULL; } - PyObject* str_repr = PyUnicode_FromFormat("('%U', '%U')", source_name, target_name); + PyObject* str_repr = PyUnicode_FromFormat("('%s', '%s')", source_name.c_str(), target_name.c_str()); - Py_XDECREF(source_name); - Py_XDECREF(target_name); return str_repr; } -PyTypeObject GraphEdgeType = { +inline PyTypeObject GraphEdgeType = { /* tp_name */ PyVarObject_HEAD_INIT(NULL, 0) "GraphEdge", /* tp_basicsize */ sizeof(GraphEdge), /* tp_itemsize */ 0, diff --git a/pydatastructs/utils/_backend/cpp/graph_bindings.hpp b/pydatastructs/utils/_backend/cpp/graph_bindings.hpp new file mode 100644 index 000000000..aeabb5ab7 --- /dev/null +++ b/pydatastructs/utils/_backend/cpp/graph_bindings.hpp @@ -0,0 +1,11 @@ +#ifndef GRAPH_BINDINGS_HPP +#define GRAPH_BINDINGS_HPP + +#include + +extern PyTypeObject AdjacencyListGraphType; +extern PyTypeObject AdjacencyListGraphNodeType; +extern PyTypeObject AdjacencyMatrixGraphType; +extern PyTypeObject AdjacencyMatrixGraphNodeType; + +#endif diff --git a/pydatastructs/utils/_backend/cpp/graph_utils.cpp b/pydatastructs/utils/_backend/cpp/graph_utils.cpp index c00c83b9d..d47390fc1 100644 --- a/pydatastructs/utils/_backend/cpp/graph_utils.cpp +++ b/pydatastructs/utils/_backend/cpp/graph_utils.cpp @@ -3,6 +3,7 @@ #include "AdjacencyListGraphNode.hpp" #include "AdjacencyMatrixGraphNode.hpp" #include "GraphEdge.hpp" +#include "graph_bindings.hpp" static struct PyModuleDef graph_utils_struct = { PyModuleDef_HEAD_INIT, diff --git a/requirements.txt b/requirements.txt index 0ea18bb95..a8b867d7c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ codecov +pytest pytest-cov diff --git a/scripts/build/dummy_submodules_data.py b/scripts/build/dummy_submodules_data.py index 115d080a2..1dde15e81 100644 --- a/scripts/build/dummy_submodules_data.py +++ b/scripts/build/dummy_submodules_data.py @@ -1,9 +1,9 @@ project = 'pydatastructs' -modules = ['linear_data_structures', 'miscellaneous_data_structures', 'utils', 'trees'] +modules = ['linear_data_structures', 'miscellaneous_data_structures', 'utils', 'trees', 'graphs'] backend = '_backend' cpp = 'cpp' -dummy_submodules_list = [('_arrays.py', '_algorithms.py'), ('_stack.py',), ('_nodes.py', '_graph_utils.py',), ('_trees.py',)] +dummy_submodules_list = [('_arrays.py', '_algorithms.py'), ('_stack.py',), ('_nodes.py', '_graph_utils.py',), ('_trees.py',), ('_graph.py',)] diff --git a/setup.py b/setup.py index 60c4ec36d..bbe3faef0 100644 --- a/setup.py +++ b/setup.py @@ -3,6 +3,7 @@ from pydatastructs import linear_data_structures from pydatastructs import miscellaneous_data_structures from pydatastructs import trees +from pydatastructs import graphs with open("README.md", "r") as fh: long_description = fh.read() @@ -13,6 +14,7 @@ extensions.extend(linear_data_structures._extensions.extensions) extensions.extend(miscellaneous_data_structures._extensions.extensions) extensions.extend(trees._extensions.extensions) +extensions.extend(graphs._extensions.extensions) setuptools.setup( name="cz-pydatastructs",