diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b16910be..e84cc8af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -177,7 +177,7 @@ jobs: update-conda: true python-version: ${{ matrix.python-version }} conda-channels: anaconda, conda-forge - # - run: conda --version # This fails due to unknown reasons + - run: which python - name: Upgrade pip version diff --git a/oryx-build-commands.txt b/oryx-build-commands.txt new file mode 100644 index 00000000..d647bdf7 --- /dev/null +++ b/oryx-build-commands.txt @@ -0,0 +1,2 @@ +PlatformWithVersion=Python +BuildCommands=conda env create --file environment.yml --prefix ./venv --quiet diff --git a/pydatastructs/graphs/_backend/cpp/__init__.py b/pydatastructs/graphs/_backend/cpp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pydatastructs/graphs/_backend/cpp/alogorithms/algorithms.cpp b/pydatastructs/graphs/_backend/cpp/alogorithms/algorithms.cpp new file mode 100644 index 00000000..8c04a2bc --- /dev/null +++ b/pydatastructs/graphs/_backend/cpp/alogorithms/algorithms.cpp @@ -0,0 +1,192 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +// Graph class to represent the graph +class Graph { +public: + // Add a node to the graph + void addNode(const std::string& nodeName) { + adjacencyList[nodeName] = std::vector(); + } + + // Add an edge between two nodes + void addEdge(const std::string& node1, const std::string& node2) { + adjacencyList[node1].push_back(node2); + adjacencyList[node2].push_back(node1); // Assuming undirected graph + } + + // Get neighbors of a node + const std::vector& getNeighbors(const std::string& node) const { + return adjacencyList.at(node); + } + + // Check if node exists + bool hasNode(const std::string& node) const { + return adjacencyList.find(node) != adjacencyList.end(); + } + +private: + std::unordered_map> adjacencyList; +}; + +// Python Graph Object Definition +typedef struct { + PyObject_HEAD + Graph graph; +} PyGraphObject; + +static PyTypeObject PyGraphType; + +// Serial BFS Implementation +static PyObject* breadth_first_search(PyObject* self, PyObject* args) { + PyGraphObject* pyGraph; + const char* sourceNode; + PyObject* pyOperation; + + if (!PyArg_ParseTuple(args, "OsO", &pyGraph, &sourceNode, &pyOperation)) { + return NULL; + } + + auto operation = [pyOperation](const std::string& currNode, const std::string& nextNode) -> bool { + PyObject* result = PyObject_CallFunction(pyOperation, "ss", currNode.c_str(), nextNode.c_str()); + if (result == NULL) { + return false; + } + bool status = PyObject_IsTrue(result); + Py_XDECREF(result); + return status; + }; + + std::unordered_map visited; + std::queue bfsQueue; + bfsQueue.push(sourceNode); + visited[sourceNode] = true; + + while (!bfsQueue.empty()) { + std::string currNode = bfsQueue.front(); + bfsQueue.pop(); + + for (const std::string& nextNode : pyGraph->graph.getNeighbors(currNode)) { + if (!visited[nextNode]) { + if (!operation(currNode, nextNode)) { + Py_RETURN_NONE; + } + bfsQueue.push(nextNode); + visited[nextNode] = true; + } + } + } + + Py_RETURN_NONE; +} + +// Parallel BFS Implementation +static PyObject* breadth_first_search_parallel(PyObject* self, PyObject* args) { + PyGraphObject* pyGraph; + const char* sourceNode; + int numThreads; + PyObject* pyOperation; + + if (!PyArg_ParseTuple(args, "OsIO", &pyGraph, &sourceNode, &numThreads, &pyOperation)) { + return NULL; + } + + auto operation = [pyOperation](const std::string& currNode, const std::string& nextNode) -> bool { + PyObject* result = PyObject_CallFunction(pyOperation, "ss", currNode.c_str(), nextNode.c_str()); + if (result == NULL) { + return false; + } + bool status = PyObject_IsTrue(result); + Py_XDECREF(result); + return status; + }; + + std::unordered_map visited; + std::queue bfsQueue; + std::mutex queueMutex; + + bfsQueue.push(sourceNode); + visited[sourceNode] = true; + + auto bfsWorker = [&](int threadId) { + while (true) { + std::string currNode; + { + std::lock_guard lock(queueMutex); + if (bfsQueue.empty()) return; + currNode = bfsQueue.front(); + bfsQueue.pop(); + } + + for (const std::string& nextNode : pyGraph->graph.getNeighbors(currNode)) { + if (!visited[nextNode]) { + if (!operation(currNode, nextNode)) { + return; + } + std::lock_guard lock(queueMutex); + bfsQueue.push(nextNode); + visited[nextNode] = true; + } + } + } + }; + + std::vector threads; + for (int i = 0; i < numThreads; ++i) { + threads.push_back(std::thread(bfsWorker, i)); + } + + for (auto& t : threads) { + t.join(); + } + + Py_RETURN_NONE; +} + +// Module Method Definitions +static PyMethodDef module_methods[] = { + {"breadth_first_search", breadth_first_search, METH_VARARGS, "Serial Breadth First Search."}, + {"breadth_first_search_parallel", breadth_first_search_parallel, METH_VARARGS, "Parallel Breadth First Search."}, + {NULL, NULL, 0, NULL} +}; + +// Python Module Definition +static struct PyModuleDef graphmodule = { + PyModuleDef_HEAD_INIT, + "_graph_algorithms", + "Graph Algorithms C++ Backend", + -1, + module_methods +}; + +// Module Initialization +PyMODINIT_FUNC PyInit__graph_algorithms(void) { + PyObject* m; + + // Initialize Graph Type + PyGraphType.tp_name = "Graph"; + PyGraphType.tp_basicsize = sizeof(PyGraphObject); + PyGraphType.tp_flags = Py_TPFLAGS_DEFAULT; + PyGraphType.tp_doc = "Graph object in C++."; + + if (PyType_Ready(&PyGraphType) < 0) { + return NULL; + } + + m = PyModule_Create(&graphmodule); + if (m == NULL) { + return NULL; + } + + // Add Graph Type to module + Py_INCREF(&PyGraphType); + PyModule_AddObject(m, "Graph", (PyObject*)&PyGraphType); + + return m; +} diff --git a/pydatastructs/graphs/algorithms.py b/pydatastructs/graphs/algorithms.py index 334f522c..ba95dd4f 100644 --- a/pydatastructs/graphs/algorithms.py +++ b/pydatastructs/graphs/algorithms.py @@ -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.alogorithms import algorithms __all__ = [ 'breadth_first_search', @@ -83,15 +84,8 @@ def breadth_first_search( """ 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) - + return algorithms.breadth_first_search(graph, source_node, operation) + def _breadth_first_search_adjacency_list( graph, source_node, operation, *args, **kwargs): bfs_queue = Queue() @@ -171,14 +165,8 @@ def breadth_first_search_parallel( """ raise_if_backend_is_not_python( breadth_first_search_parallel, kwargs.get('backend', Backend.PYTHON)) - import pydatastructs.graphs.algorithms as algorithms - func = "_breadth_first_search_parallel_" + 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, num_threads, operation, *args, **kwargs) + return algorithms.breadth_first_search_parallel(graph, source_node, num_threads, operation) + def _generate_layer(**kwargs): _args, _kwargs = kwargs.get('args'), kwargs.get('kwargs') diff --git a/pydatastructs/multi_threaded_algorithms/__init__.py b/pydatastructs/multi_threaded_algorithms/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pydatastructs/multi_threaded_algorithms/fibonacci.py b/pydatastructs/multi_threaded_algorithms/fibonacci.py new file mode 100644 index 00000000..9f960569 --- /dev/null +++ b/pydatastructs/multi_threaded_algorithms/fibonacci.py @@ -0,0 +1,58 @@ +import threading + +class Fibonacci: + """Representation of Fibonacci data structure + + Parameters + ---------- + n : int + The index for which to compute the Fibonacci number. + backend : str + Optional, by default 'python'. Specifies whether to use the Python implementation or another backend. + """ + + def __init__(self, n, backend='python'): + if n<0: + raise ValueError("n cannot be negative") # Checking invalid input + self.n = n + self.backend = backend + self.result = [None] * (n + 1) # To store Fibonacci numbers + self.threads = [] # List to store thread references + # Check for valid backend + if backend != 'python': + raise NotImplementedError(f"Backend '{backend}' is not implemented.") + + def fib(self, i): + """Calculates the Fibonacci number recursively and stores it in result.""" + if i <= 1: + return i + if self.result[i] is not None: + return self.result[i] + self.result[i] = self.fib(i - 1) + self.fib(i - 2) + return self.result[i] + + def threaded_fib(self, i): + """Wrapper function to calculate Fibonacci in a thread and store the result.""" + self.result[i] = self.fib(i) + + def calculate(self): + """Calculates the Fibonacci sequence for all numbers up to n using multi-threading.""" + # Start threads for each Fibonacci number calculation + for i in range(self.n + 1): + thread = threading.Thread(target=self.threaded_fib, args=(i,)) + self.threads.append(thread) + thread.start() + + # Wait for all threads to complete + for thread in self.threads: + thread.join() + + # Return the nth Fibonacci number after all threads complete + return self.result[self.n] + + @property + def sequence(self): + """Returns the Fibonacci sequence up to the nth number.""" + if self.result[0] is None: + self.calculate() + return self.result[:self.n + 1] diff --git a/pydatastructs/multi_threaded_algorithms/tests/test_fibonacci.py b/pydatastructs/multi_threaded_algorithms/tests/test_fibonacci.py new file mode 100644 index 00000000..ff8ea5df --- /dev/null +++ b/pydatastructs/multi_threaded_algorithms/tests/test_fibonacci.py @@ -0,0 +1,85 @@ +from pydatastructs.multi_threaded_algorithms.fibonacci import Fibonacci +import threading +from pydatastructs.utils.raises_util import raises + + +def test_Fibonacci(): + # Test for the Fibonacci class with default Python backend + f = Fibonacci(20) + assert isinstance(f, Fibonacci) + assert f.n == 20 + assert f.calculate() == 6765 # Fibonacci(20) + + # Test with different n values + f1 = Fibonacci(7) + assert f1.calculate() == 13 # Fibonacci(7) + + f2 = Fibonacci(0) + assert f2.calculate() == 0 # Fibonacci(0) + + f3 = Fibonacci(1) + assert f3.calculate() == 1 # Fibonacci(1) + + # Test for full Fibonacci sequence up to n + assert f.sequence == [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765] + + # Test for larger Fibonacci number + f_large = Fibonacci(100) + assert f_large.calculate() == 354224848179261915075 # Fibonacci(100) + + # Test for sequence with larger n values + assert len(f_large.sequence) == 101 # Fibonacci sequence up to 100 should have 101 elements + +def test_Fibonacci_with_threading(): + # Test for multi-threading Fibonacci calculation for small numbers + f_small = Fibonacci(10) + result_small = f_small.calculate() + assert result_small == 55 # Fibonacci(10) + + # Test for multi-threading Fibonacci calculation with medium size n + f_medium = Fibonacci(30) + result_medium = f_medium.calculate() + assert result_medium == 832040 # Fibonacci(30) + + # Test for multi-threading Fibonacci calculation with large n + f_large = Fibonacci(50) + result_large = f_large.calculate() + assert result_large == 12586269025 # Fibonacci(50) + + # Test the Fibonacci sequence correctness for medium size n + assert f_medium.sequence == [ + 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, + 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040 + ] + + # Check that sequence length is correct for large n (e.g., Fibonacci(50)) + assert len(f_large.sequence) == 51 # Fibonacci sequence up to 50 should have 51 elements + + # Test invalid input (n cannot be negative) + assert raises(ValueError, lambda: Fibonacci(-5)) + + # Test when backend is set to CPP (this part assumes a proper backend, can be skipped if not implemented) + f_cpp = Fibonacci(10, backend='python') + result_cpp = f_cpp.calculate() + assert result_cpp == 55 # Fibonacci(10) should be the same result as Python + + # Test if sequence matches expected for small number of terms + f_test = Fibonacci(5) + assert f_test.sequence == [0, 1, 1, 2, 3, 5] + +def test_Fibonacci_with_invalid_backend(): + # Test when an invalid backend is provided (should raise an error) + assert raises(NotImplementedError, lambda: Fibonacci(20, backend='invalid_backend')) + +def test_Fibonacci_with_threads(): + # Test multi-threaded calculation is correct for different n + f_threaded = Fibonacci(25) + assert f_threaded.calculate() == 75025 # Fibonacci(25) + # Validate that the thread pool handles large n correctly + f_threaded_large = Fibonacci(40) + assert f_threaded_large.calculate() == 102334155 # Fibonacci(40) + # Ensure that no threads are left hanging (checks for thread cleanup) + threads_before = threading.active_count() + f_threaded.calculate() + threads_after = threading.active_count() + assert threads_before == threads_after # No new threads should be created unexpectedly