diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 8a93fb8f6..7bd2f2ce0 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -4069,6 +4069,21 @@ int igraphmodule_PyObject_to_pagerank_algo_t(PyObject *o, igraph_pagerank_algo_t TRANSLATE_ENUM_WITH(pagerank_algo_tt); } +/** + * \ingroup python_interface_conversion + * \brief Converts a Python object to an igraph \c igraph_metric_t + */ +int igraphmodule_PyObject_to_metric_t(PyObject *o, igraph_metric_t *result) { + static igraphmodule_enum_translation_table_entry_t metric_tt[] = { + {"euclidean", IGRAPH_METRIC_EUCLIDEAN}, + {"l2", IGRAPH_METRIC_L2}, /* alias to the previous */ + {"manhattan", IGRAPH_METRIC_MANHATTAN}, + {"l1", IGRAPH_METRIC_L1}, /* alias to the previous */ + {0,0} + }; + TRANSLATE_ENUM_WITH(metric_tt); +} + /** * \ingroup python_interface_conversion * \brief Converts a Python object to an igraph \c igraph_edge_type_sw_t diff --git a/src/_igraph/convert.h b/src/_igraph/convert.h index a7ca02b16..4b1a0731b 100644 --- a/src/_igraph/convert.h +++ b/src/_igraph/convert.h @@ -78,6 +78,7 @@ int igraphmodule_PyObject_to_laplacian_normalization_t(PyObject *o, igraph_lapla int igraphmodule_PyObject_to_layout_grid_t(PyObject *o, igraph_layout_grid_t *result); int igraphmodule_PyObject_to_lpa_variant_t(PyObject *o, igraph_lpa_variant_t *result); int igraphmodule_PyObject_to_loops_t(PyObject *o, igraph_loops_t *result); +int igraphmodule_PyObject_to_metric_t(PyObject *o, igraph_metric_t *result); int igraphmodule_PyObject_to_mst_algorithm_t(PyObject *o, igraph_mst_algorithm_t *result); int igraphmodule_PyObject_to_neimode_t(PyObject *o, igraph_neimode_t *result); int igraphmodule_PyObject_to_pagerank_algo_t(PyObject *o, igraph_pagerank_algo_t *result); diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 19bae50fc..bd88fed4d 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -14034,6 +14034,46 @@ PyObject *igraphmodule_Graph_random_walk(igraphmodule_GraphObject * self, } } +/********************************************************************** + * Spatial graphs * + **********************************************************************/ + +PyObject *igraphmodule_Graph_Nearest_Neighbor_Graph(PyTypeObject *type, + PyObject *args, PyObject *kwds) { + static char *kwlist[] = {"points", "k", "r", "metric", "directed", NULL}; + PyObject *points_o = Py_None, *metric_o = Py_None, *directed_o = Py_False; + double r = -1; + Py_ssize_t k = 1; + igraph_matrix_t points; + igraphmodule_GraphObject *self; + igraph_t graph; + igraph_metric_t metric = IGRAPH_METRIC_EUCLIDEAN; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|ndOO", kwlist, + &points_o, &k, &r, &metric_o, &directed_o)) { + return NULL; + } + + if (igraphmodule_PyObject_to_metric_t(metric_o, &metric)) { + return NULL; + } + + if (igraphmodule_PyObject_to_matrix_t(points_o, &points, "points")) { + return NULL; + } + + if (igraph_nearest_neighbor_graph(&graph, &points, metric, k, r, PyObject_IsTrue(directed_o))) { + igraph_matrix_destroy(&points); + return igraphmodule_handle_igraph_error(); + } + + igraph_matrix_destroy(&points); + + CREATE_GRAPH_FROM_TYPE(self, graph, type); + + return (PyObject *) self; +} + /********************************************************************** * Special internal methods that you won't need to mess around with * **********************************************************************/ @@ -18894,6 +18934,22 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " the given length (shorter if the random walk got stuck).\n" }, + /**********************/ + /* SPATIAL GRAPHS */ + /**********************/ + {"Nearest_Neighbor_Graph", (PyCFunction)igraphmodule_Graph_Nearest_Neighbor_Graph, + METH_VARARGS | METH_CLASS | METH_KEYWORDS, + "Nearest_Neighbor_Graph(points, k=1, r=-1, metric=\"euclidean\", directed=False)\n--\n\n" + "Constructs a k nearest neighbor graph of a give point set. Each point is\n" + "connected to at most k spatial neighbors within a radius of 1.\n\n" + "@param points: coordinates of the points to use, in an arbitrary number of dimensions\n" + "@param k: at most how many neighbors to connect to. Pass a negative value to ignore\n" + "@param r: only neighbors within this radius are considered. Pass a negative value to ignore\n" + "@param metric: the metric to use. C{\"euclidean\"} and C{\"manhattan\"} are supported.\n" + "@param directed: whethe to create directed edges.\n" + "@return: the nearest neighbor graph.\n" + }, + /**********************/ /* INTERNAL FUNCTIONS */ /**********************/ diff --git a/tests/test_generators.py b/tests/test_generators.py index 675de880a..c09b7c914 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -890,6 +890,27 @@ def testDataFrame(self): edges = pd.DataFrame(np.array([[0, 1], [1, np.nan], [1, 2]]), dtype="Int64") Graph.DataFrame(edges) + def testNearestNeighborGraph(self): + points = [[0,0], [1,2], [-3, -3]] + + g = Graph.Nearest_Neighbor_Graph(points) + # expecting 1 - 2, 3 - 1 + self.assertFalse(g.is_directed()) + self.assertEqual(g.vcount(), 3) + self.assertEqual(g.ecount(), 2) + + g = Graph.Nearest_Neighbor_Graph(points, directed=True) + # expecting 1 <-> 2, 3 -> 1 + self.assertTrue(g.is_directed()) + self.assertEqual(g.vcount(), 3) + self.assertEqual(g.ecount(), 3) + + # expecting a complete graph + g = Graph.Nearest_Neighbor_Graph(points, k=2) + self.assertFalse(g.is_directed()) + self.assertEqual(g.vcount(), 3) + self.assertTrue(g.is_complete()) + def suite(): generator_suite = unittest.defaultTestLoader.loadTestsFromTestCase(GeneratorTests)