diff --git a/CHANGELOG.md b/CHANGELOG.md index dea92015e..88c1c9d7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Dropped support for Python 3.8 as it has now reached its end of life. - The C core of igraph was updated to version 0.10.15. +- Added `Graph.simple_cycles()` to find simple cycles in the graph. ## [0.11.8] - 2024-10-25 diff --git a/pyproject.toml b/pyproject.toml index d62d390de..db2454afb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,8 @@ requires = [ # Workaround based on this commit: # https://github.com/harfbuzz/uharfbuzz/commit/9b607bd06fb17fcb4abe3eab5c4f342ad08309d7 "setuptools>=64,<72.2.0; platform_python_implementation == 'PyPy'", - "setuptools>=64; platform_python_implementation != 'PyPy'" + "setuptools>=64; platform_python_implementation != 'PyPy'", + "cmake>=3.12" ] build-backend = "setuptools.build_meta" diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 95a29e5ff..f8185427e 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -7813,6 +7813,60 @@ PyObject *igraphmodule_Graph_minimum_cycle_basis( return result_o; } + +PyObject *igraphmodule_Graph_simple_cycles( + igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds +) { + PyObject *mode_o = Py_None; + PyObject *min_cycle_length_o = Py_None; + PyObject *max_cycle_length_o = Py_None; + + // argument defaults: no cycle limits + igraph_integer_t mode = IGRAPH_OUT; + igraph_integer_t min_cycle_length = -1; + igraph_integer_t max_cycle_length = -1; + + static char *kwlist[] = { "mode", "min_cycle_length", "max_cycle_length", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, &mode_o, &min_cycle_length_o, &max_cycle_length_o)) + return NULL; + + if (mode_o != Py_None && igraphmodule_PyObject_to_integer_t(mode_o, &mode)) + return NULL; + + if (min_cycle_length_o != Py_None && igraphmodule_PyObject_to_integer_t(min_cycle_length_o, &min_cycle_length)) + return NULL; + + if (max_cycle_length_o != Py_None && igraphmodule_PyObject_to_integer_t(max_cycle_length_o, &max_cycle_length)) + return NULL; + + igraph_vector_int_list_t vertices; + igraph_vector_int_list_init(&vertices, 0); + igraph_vector_int_list_t edges; + igraph_vector_int_list_init(&edges, 0); + + if (igraph_simple_cycles( + &self->g, &vertices, &edges, mode, min_cycle_length, max_cycle_length + )) { + igraph_vector_int_list_destroy(&vertices); + igraph_vector_int_list_destroy(&edges); + igraphmodule_handle_igraph_error(); + return NULL; + } + + PyObject *result_edges_o; + result_edges_o = igraphmodule_vector_int_list_t_to_PyList_of_tuples(&edges); + igraph_vector_int_list_destroy(&edges); + + PyObject *result_vertices_o; + result_vertices_o = igraphmodule_vector_int_list_t_to_PyList_of_tuples(&vertices); + igraph_vector_int_list_destroy(&vertices); + + PyObject *results; + results = Py_BuildValue("(OO)", result_vertices_o, result_edges_o); + return results; +} + /********************************************************************** * Graph layout algorithms * **********************************************************************/ @@ -16563,6 +16617,23 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " no guarantees are given about the ordering of edge IDs within cycles.\n" "@return: the cycle basis as a list of tuples containing edge IDs" }, + {"simple_cycles", (PyCFunction) igraphmodule_Graph_simple_cycles, + METH_VARARGS | METH_KEYWORDS, + "simple_cycles(mode=None, min_cycle_length=0, max_cycle_length=-1)\n--\n\n" + "Finds simple cycles in a graph\n\n" + "@param mode: for directed graphs, specifies how the edge directions\n" + " should be taken into account. C{\"all\"} means that the edge directions\n" + " must be ignored, C{\"out\"} means that the edges must be oriented away\n" + " from the root, C{\"in\"} means that the edges must be oriented\n" + " towards the root. Ignored for undirected graphs.\n" + "@param min_cycle_length: the minimum number of vertices in a cycle\n" + " for it to be returned.\n" + "@param max_cycle_length: the maximum number of vertices in a cycle\n" + " for it to be considered.\n" + "@return: vertices, edges: a tuple with the vertices and edges, \n" + " respectively, each as a list of tuples containing edge and vertex\n" + " IDs, respectively." + }, /********************/ /* LAYOUT FUNCTIONS */ diff --git a/tests/test_cycles.py b/tests/test_cycles.py index 48a37f2f7..191276276 100644 --- a/tests/test_cycles.py +++ b/tests/test_cycles.py @@ -60,6 +60,25 @@ def test_fundamental_cycles(self): ] assert cycles == [[6, 7, 10], [8, 9, 10]] + def test_simple_cycles(self): + g = Graph( + [ + (0, 1), + (1, 2), + (2, 0), + (0, 0), + (0, 3), + (3, 4), + (4, 5), + (5, 0), + ] + ) + + vertices, edges = g.simple_cycles() + assert len(vertices) == 3 + assert len(edges) == 3 + + def test_minimum_cycle_basis(self): g = Graph( [