Skip to content

Commit 2baed11

Browse files
committed
feat: added more modes for Graph.to_directed(), closes #376
1 parent 2054f46 commit 2baed11

5 files changed

Lines changed: 117 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111

1212
* Added `Graph.Tree_Game()` to generate random trees with uniform sampling.
1313

14+
* `Graph.to_directed()` now supports a `mode=...` keyword argument.
15+
16+
### Deprecated
17+
18+
* `Graph.to_directed(mutual=...)` is now deprecated, use `mode=...` instead.
19+
1420
## 0.9.1
1521

1622
### Changed

src/_igraph/convert.c

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,31 @@ int igraphmodule_PyObject_to_subgraph_implementation_t(PyObject *o,
641641
return igraphmodule_PyObject_to_enum(o, subgraph_impl_tt, (int*)result);
642642
}
643643

644+
/**
645+
* \ingroup python_interface_conversion
646+
* \brief Converts a Python object to an igraph \c igraph_to_directed_t
647+
*/
648+
int igraphmodule_PyObject_to_to_directed_t(PyObject *o,
649+
igraph_to_directed_t *result) {
650+
static igraphmodule_enum_translation_table_entry_t to_directed_tt[] = {
651+
{"acyclic", IGRAPH_TO_DIRECTED_ACYCLIC},
652+
{"arbitrary", IGRAPH_TO_DIRECTED_ARBITRARY},
653+
{"mutual", IGRAPH_TO_DIRECTED_MUTUAL},
654+
{"random", IGRAPH_TO_DIRECTED_RANDOM},
655+
{0,0}
656+
};
657+
658+
if (o == Py_True) {
659+
*result = IGRAPH_TO_DIRECTED_MUTUAL;
660+
return 0;
661+
} else if (o == Py_False) {
662+
*result = IGRAPH_TO_DIRECTED_ARBITRARY;
663+
return 0;
664+
}
665+
666+
return igraphmodule_PyObject_to_enum(o, to_directed_tt, (int*)result);
667+
}
668+
644669
/**
645670
* \ingroup python_interface_conversion
646671
* \brief Converts a Python object to an igraph \c igraph_to_undirected_t

src/_igraph/convert.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ int igraphmodule_PyObject_to_spinglass_implementation_t(PyObject *o, igraph_spin
7575
int igraphmodule_PyObject_to_spincomm_update_t(PyObject *o, igraph_spincomm_update_t *result);
7676
int igraphmodule_PyObject_to_star_mode_t(PyObject *o, igraph_star_mode_t *result);
7777
int igraphmodule_PyObject_to_subgraph_implementation_t(PyObject *o, igraph_subgraph_implementation_t *result);
78+
int igraphmodule_PyObject_to_to_directed_t(PyObject *o, igraph_to_directed_t *result);
7879
int igraphmodule_PyObject_to_to_undirected_t(PyObject *o, igraph_to_undirected_t *result);
7980
int igraphmodule_PyObject_to_transitivity_mode_t(PyObject *o, igraph_transitivity_mode_t *result);
8081
int igraphmodule_PyObject_to_tree_mode_t(PyObject *o, igraph_tree_mode_t *result);

src/_igraph/graphobject.c

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -608,9 +608,9 @@ PyObject *igraphmodule_Graph_delete_vertices(igraphmodule_GraphObject * self,
608608

609609
/*Py_None also means all for now, but it is deprecated */
610610
if (list == Py_None) {
611-
PyErr_Warn(PyExc_DeprecationWarning, "Graph.delete_vertices(None) is "
612-
"deprecated since igraph 0.8.3, please use "
613-
"Graph.delete_vertices() instead");
611+
PyErr_Warn(PyExc_DeprecationWarning, "Graph.delete_vertices(None) is "
612+
"deprecated since igraph 0.8.3, please use "
613+
"Graph.delete_vertices() instead");
614614
}
615615

616616
/* this already converts no arguments and Py_None to all vertices */
@@ -7850,18 +7850,36 @@ PyObject *igraphmodule_Graph_to_undirected(igraphmodule_GraphObject * self,
78507850
PyObject *igraphmodule_Graph_to_directed(igraphmodule_GraphObject * self,
78517851
PyObject * args, PyObject * kwds)
78527852
{
7853-
PyObject *mutual = Py_True;
7853+
PyObject *mutual_o = Py_None;
7854+
PyObject *mode_o = Py_None;
78547855
igraph_to_directed_t mode = IGRAPH_TO_DIRECTED_MUTUAL;
7855-
static char *kwlist[] = { "mutual", NULL };
7856-
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &mutual))
7856+
static char *kwlist[] = { "mode", "mutual", NULL };
7857+
7858+
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &mode_o, &mutual_o))
78577859
return NULL;
7858-
mode =
7859-
(PyObject_IsTrue(mutual) ? IGRAPH_TO_DIRECTED_MUTUAL :
7860-
IGRAPH_TO_DIRECTED_ARBITRARY);
7860+
7861+
if (mode_o == Py_None) {
7862+
/* mode argument omitted so we fall back to 'mutual' for sake of
7863+
* compatibility and print a warning */
7864+
if (mutual_o == Py_None) {
7865+
/* mutual was not given either, so this is okay */
7866+
mode = IGRAPH_TO_DIRECTED_MUTUAL;
7867+
} else {
7868+
mode = PyObject_IsTrue(mutual_o) ? IGRAPH_TO_DIRECTED_MUTUAL : IGRAPH_TO_DIRECTED_ARBITRARY;
7869+
PyErr_Warn(PyExc_DeprecationWarning, "The 'mutual' argument is deprecated since "
7870+
"igraph 0.9.3, please use mode=... instead");
7871+
}
7872+
} else {
7873+
if (igraphmodule_PyObject_to_to_directed_t(mode_o, &mode)) {
7874+
return NULL;
7875+
}
7876+
}
7877+
78617878
if (igraph_to_directed(&self->g, mode)) {
78627879
igraphmodule_handle_igraph_error();
78637880
return NULL;
78647881
}
7882+
78657883
Py_RETURN_NONE;
78667884
}
78677885

@@ -14802,14 +14820,18 @@ struct PyMethodDef igraphmodule_Graph_methods[] = {
1480214820
"Internal function, undocumented.\n\n"
1480314821
"@see: Graph.get_incidence()\n\n"},
1480414822

14805-
// interface to igraph_to_directed
14823+
/* interface to igraph_to_directed */
1480614824
{"to_directed", (PyCFunction) igraphmodule_Graph_to_directed,
1480714825
METH_VARARGS | METH_KEYWORDS,
14808-
"to_directed(mutual=True)\n--\n\n"
14826+
"to_directed(mode=\"mutual\")\n--\n\n"
1480914827
"Converts an undirected graph to directed.\n\n"
14810-
"@param mutual: C{True} if mutual directed edges should be\n"
14811-
" created for every undirected edge. If C{False}, a directed\n"
14812-
" edge with arbitrary direction is created.\n"},
14828+
"@param mode: specifies how to convert undirected edges into\n"
14829+
" directed ones. C{True} or C{\"mutual\"} creates a mutual edge pair\n"
14830+
" for each undirected edge. C{False} or C{\"arbitrary\"} picks an\n"
14831+
" arbitrary (but deterministic) edge direction for each edge.\n"
14832+
" C{\"random\"} picks a random direction for each edge. C{\"acyclic\"}\n"
14833+
" picks the edge directions in a way that the resulting graph will be\n"
14834+
" acyclic if there were no self-loops in the original graph.\n"
1481314835

1481414836
// interface to igraph_to_undirected
1481514837
{"to_undirected", (PyCFunction) igraphmodule_Graph_to_undirected,

tests/test_conversion.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import random
12
import unittest
2-
from igraph import *
3+
4+
from igraph import Graph, Matrix
35

46

57
class DirectedUndirectedTests(unittest.TestCase):
@@ -37,7 +39,7 @@ def testToUndirected(self):
3739
graph2.es["weight"] == [7, 3, 11] or graph2.es["weight"] == [3, 7, 11]
3840
)
3941

40-
def testToDirected(self):
42+
def testToDirectedNoModeArg(self):
4143
graph = Graph([(0, 1), (0, 2), (2, 3), (2, 4)], directed=False)
4244
graph.to_directed()
4345
self.assertTrue(graph.is_directed())
@@ -47,6 +49,51 @@ def testToDirected(self):
4749
== [(0, 1), (0, 2), (1, 0), (2, 0), (2, 3), (2, 4), (3, 2), (4, 2)]
4850
)
4951

52+
def testToDirectedMutual(self):
53+
graph = Graph([(0, 1), (0, 2), (2, 3), (2, 4)], directed=False)
54+
graph.to_directed("mutual")
55+
self.assertTrue(graph.is_directed())
56+
self.assertTrue(graph.vcount() == 5)
57+
self.assertTrue(
58+
sorted(graph.get_edgelist())
59+
== [(0, 1), (0, 2), (1, 0), (2, 0), (2, 3), (2, 4), (3, 2), (4, 2)]
60+
)
61+
62+
def testToDirectedAcyclic(self):
63+
graph = Graph([(0, 1), (2, 0), (3, 0), (3, 0), (4, 2)], directed=False)
64+
graph.to_directed("acyclic")
65+
self.assertTrue(graph.is_directed())
66+
self.assertTrue(graph.vcount() == 5)
67+
print(graph.get_edgelist())
68+
self.assertTrue(
69+
sorted(graph.get_edgelist())
70+
== [(0, 1), (0, 2), (0, 3), (0, 3), (2, 4)]
71+
)
72+
73+
def testToDirectedRandom(self):
74+
random.seed(0)
75+
76+
graph = Graph.Ring(200, directed=False)
77+
graph.to_directed("random")
78+
79+
self.assertTrue(graph.is_directed())
80+
self.assertTrue(graph.vcount() == 200)
81+
edgelist1 = sorted(graph.get_edgelist())
82+
83+
graph = Graph.Ring(200, directed=False)
84+
graph.to_directed("random")
85+
86+
self.assertTrue(graph.is_directed())
87+
self.assertTrue(graph.vcount() == 200)
88+
edgelist2 = sorted(graph.get_edgelist())
89+
90+
self.assertTrue(edgelist1 != edgelist2)
91+
92+
def testToDirectedInvalidMode(self):
93+
graph = Graph([(0, 1), (0, 2), (2, 3), (2, 4)], directed=False)
94+
with self.assertRaises(ValueError):
95+
graph.to_directed("no-such-mode")
96+
5097

5198
class GraphRepresentationTests(unittest.TestCase):
5299
def testGetAdjacency(self):

0 commit comments

Comments
 (0)