Skip to content
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

Solved issue #475 Added fibonacci.py and test_fibonacci.py #593

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions oryx-build-commands.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
PlatformWithVersion=Python
BuildCommands=conda env create --file environment.yml --prefix ./venv --quiet
Empty file.
192 changes: 192 additions & 0 deletions pydatastructs/graphs/_backend/cpp/alogorithms/algorithms.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
#include <Python.h>
#include <iostream>
#include <unordered_map>
#include <vector>
#include <queue>
#include <thread>
#include <functional>
#include <mutex>

// 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<std::string>();
}

// 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<std::string>& 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<std::string, std::vector<std::string>> 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<std::string, bool> visited;
std::queue<std::string> 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<std::string, bool> visited;
std::queue<std::string> bfsQueue;
std::mutex queueMutex;

bfsQueue.push(sourceNode);
visited[sourceNode] = true;

auto bfsWorker = [&](int threadId) {
while (true) {
std::string currNode;
{
std::lock_guard<std::mutex> 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<std::mutex> lock(queueMutex);
bfsQueue.push(nextNode);
visited[nextNode] = true;
}
}
}
};

std::vector<std::thread> 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;
}
22 changes: 5 additions & 17 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.alogorithms import algorithms

__all__ = [
'breadth_first_search',
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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')
Expand Down
Empty file.
58 changes: 58 additions & 0 deletions pydatastructs/multi_threaded_algorithms/fibonacci.py
Original file line number Diff line number Diff line change
@@ -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]
85 changes: 85 additions & 0 deletions pydatastructs/multi_threaded_algorithms/tests/test_fibonacci.py
Original file line number Diff line number Diff line change
@@ -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
Loading