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/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 95a29e5ff..00c3e4343 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -7813,6 +7813,65 @@ 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 *output_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; + igraph_bool_t use_edges = false; + + static char *kwlist[] = { "mode", "min", "max", "output", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO", kwlist, &mode_o, &min_cycle_length_o, &max_cycle_length_o, &output_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; + + if (igraphmodule_PyObject_to_vpath_or_epath(output_o, &use_edges)) + 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, use_edges ? NULL : &vertices, use_edges ? &edges : NULL, 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_o; + + if (use_edges) { + result_o = igraphmodule_vector_int_list_t_to_PyList_of_tuples(&edges); + } else { + result_o = igraphmodule_vector_int_list_t_to_PyList_of_tuples(&vertices); + } + igraph_vector_int_list_destroy(&edges); + igraph_vector_int_list_destroy(&vertices); + + return result_o; +} + /********************************************************************** * Graph layout algorithms * **********************************************************************/ @@ -16563,6 +16622,24 @@ 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=-1, max=-1, output=\"epath\")\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: the minimum number of vertices in a cycle\n" + " for it to be returned.\n" + "@param max: the maximum number of vertices in a cycle\n" + " for it to be considered.\n" + "@param output: determines what should be returned. If this is\n" + " C{\"vpath\"}, a list of tuples of vertex IDs will be returned. If this is\n" + " C{\"epath\"}, edge IDs are returned instead of vertex IDs.\n" + "@return: see the documentation of the C{output} parameter.\n" + }, /********************/ /* LAYOUT FUNCTIONS */ diff --git a/tests/test_cycles.py b/tests/test_cycles.py index 48a37f2f7..e3549121c 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 = g.simple_cycles(output="vpath") + edges = g.simple_cycles(output="epath") + assert len(vertices) == 3 + assert len(edges) == 3 + def test_minimum_cycle_basis(self): g = Graph( [