Skip to content

Commit cf09bb4

Browse files
committed
feat: allow zero-copy conversion from NumPy arrays to igraph vector views where possible
1 parent e911635 commit cf09bb4

File tree

8 files changed

+213
-35
lines changed

8 files changed

+213
-35
lines changed

src/codegen/internal_lib.py.in

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ igraph_vector_init_array.restype = handle_igraph_error_t
7575
igraph_vector_init_array.argtypes = [POINTER(igraph_vector_t), POINTER(igraph_real_t), igraph_integer_t]
7676

7777
igraph_vector_view = _lib.igraph_vector_view
78-
igraph_vector_view.restype = handle_igraph_error_t
78+
igraph_vector_view.restype = POINTER(igraph_vector_t)
7979
igraph_vector_view.argtypes = [POINTER(igraph_vector_t), POINTER(igraph_real_t), igraph_integer_t]
8080

8181
igraph_vector_e = _lib.igraph_vector_e
@@ -109,7 +109,7 @@ igraph_vector_int_init_array.restype = handle_igraph_error_t
109109
igraph_vector_int_init_array.argtypes = [POINTER(igraph_vector_int_t), POINTER(igraph_integer_t), igraph_integer_t]
110110

111111
igraph_vector_int_view = _lib.igraph_vector_int_view
112-
igraph_vector_int_view.restype = handle_igraph_error_t
112+
igraph_vector_int_view.restype = POINTER(igraph_vector_int_t)
113113
igraph_vector_int_view.argtypes = [POINTER(igraph_vector_int_t), POINTER(igraph_integer_t), igraph_integer_t]
114114

115115
igraph_vector_int_e = _lib.igraph_vector_int_e
@@ -143,7 +143,7 @@ igraph_vector_bool_init_array.restype = handle_igraph_error_t
143143
igraph_vector_bool_init_array.argtypes = [POINTER(igraph_vector_bool_t), POINTER(igraph_bool_t), igraph_integer_t]
144144

145145
igraph_vector_bool_view = _lib.igraph_vector_bool_view
146-
igraph_vector_bool_view.restype = handle_igraph_error_t
146+
igraph_vector_bool_view.restype = POINTER(igraph_vector_bool_t)
147147
igraph_vector_bool_view.argtypes = [POINTER(igraph_vector_bool_t), POINTER(igraph_bool_t), igraph_integer_t]
148148

149149
igraph_vector_bool_e = _lib.igraph_vector_bool_e
@@ -187,7 +187,7 @@ igraph_matrix_init_array.argtypes = [
187187
]
188188

189189
igraph_matrix_view = _lib.igraph_matrix_view
190-
igraph_matrix_view.restype = handle_igraph_error_t
190+
igraph_matrix_view.restype = POINTER(igraph_matrix_t)
191191
igraph_matrix_view.argtypes = [
192192
POINTER(igraph_matrix_t),
193193
POINTER(igraph_real_t),
@@ -236,7 +236,7 @@ igraph_matrix_int_init_array.argtypes = [
236236
]
237237

238238
igraph_matrix_int_view = _lib.igraph_matrix_int_view
239-
igraph_matrix_int_view.restype = handle_igraph_error_t
239+
igraph_matrix_int_view.restype = POINTER(igraph_matrix_int_t)
240240
igraph_matrix_int_view.argtypes = [
241241
POINTER(igraph_matrix_int_t),
242242
POINTER(igraph_integer_t),

src/igraph_ctypes/_internal/conversion.py

Lines changed: 94 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,7 @@ def edge_weights_to_igraph_vector_t_view(
161161
When the input is `None`, the return value will also be `None`, which is
162162
interpreted by the C core of igraph as all edges having equal weight.
163163
"""
164-
return (
165-
edge_weights_to_igraph_vector_t(weights, graph) if weights is not None else None
166-
)
164+
return iterable_to_igraph_vector_t_view(weights) if weights is not None else None
167165

168166

169167
def iterable_edge_indices_to_igraph_vector_int_t(
@@ -173,7 +171,7 @@ def iterable_edge_indices_to_igraph_vector_int_t(
173171
of edge IDs.
174172
"""
175173
if isinstance(indices, np.ndarray):
176-
return numpy_array_to_igraph_vector_int_t(indices)
174+
return numpy_array_to_igraph_vector_int_t(indices, flatten=True)
177175

178176
result: _VectorInt = _VectorInt.create(0)
179177
for index in indices:
@@ -199,16 +197,18 @@ def iterable_to_igraph_vector_bool_t_view(items: Iterable[Any]) -> _VectorBool:
199197
booleans based on their truth values, possibly creating a shallow view if
200198
the input is an appropriate NumPy array.
201199
"""
202-
# TODO(ntamas)
203-
return iterable_to_igraph_vector_bool_t(items)
200+
if isinstance(items, np.ndarray):
201+
return numpy_array_to_igraph_vector_bool_t_view(items)
202+
else:
203+
return iterable_to_igraph_vector_bool_t(items)
204204

205205

206206
def iterable_to_igraph_vector_int_t(items: Iterable[int]) -> _VectorInt:
207207
"""Converts an iterable containing Python integers to an igraph vector of
208208
integers.
209209
"""
210210
if isinstance(items, np.ndarray):
211-
return numpy_array_to_igraph_vector_int_t(items)
211+
return numpy_array_to_igraph_vector_int_t(items, flatten=True)
212212
else:
213213
result = _VectorInt.create(0)
214214
for item in items:
@@ -221,27 +221,34 @@ def iterable_to_igraph_vector_int_t_view(items: Iterable[Any]) -> _VectorInt:
221221
integers, possibly creating a shallow view if the input is an appropriate
222222
NumPy array.
223223
"""
224-
# TODO(ntamas)
225-
return iterable_to_igraph_vector_int_t(items)
224+
if isinstance(items, np.ndarray):
225+
return numpy_array_to_igraph_vector_int_t_view(items, flatten=True)
226+
else:
227+
return iterable_to_igraph_vector_int_t(items)
226228

227229

228230
def iterable_to_igraph_vector_t(items: Iterable[float]) -> _Vector:
229231
"""Converts an iterable containing Python integers or floats to an igraph
230232
vector of floats.
231233
"""
232-
result: _Vector = _Vector.create(0)
233-
for item in items:
234-
igraph_vector_push_back(result, item)
235-
return result
234+
if isinstance(items, np.ndarray):
235+
return numpy_array_to_igraph_vector_t(items)
236+
else:
237+
result: _Vector = _Vector.create(0)
238+
for item in items:
239+
igraph_vector_push_back(result, item)
240+
return result
236241

237242

238243
def iterable_to_igraph_vector_t_view(items: Iterable[float]) -> _Vector:
239244
"""Converts an iterable containing Python integers or floats to an igraph
240245
vector of floats, possibly creating a shallow view if the input is an
241246
appropriate NumPy array.
242247
"""
243-
# TODO(ntamas)
244-
return iterable_to_igraph_vector_t(items)
248+
if isinstance(items, np.ndarray):
249+
return numpy_array_to_igraph_vector_t_view(items)
250+
else:
251+
return iterable_to_igraph_vector_t(items)
245252

246253

247254
def iterable_vertex_indices_to_igraph_vector_int_t(
@@ -251,7 +258,7 @@ def iterable_vertex_indices_to_igraph_vector_int_t(
251258
of vertex IDs.
252259
"""
253260
if isinstance(indices, np.ndarray):
254-
return numpy_array_to_igraph_vector_int_t(indices)
261+
return numpy_array_to_igraph_vector_int_t(indices, flatten=True)
255262

256263
result: _VectorInt = _VectorInt.create(0)
257264
for index in indices:
@@ -322,15 +329,21 @@ def _ensure_matrix(items: Sequence[Sequence[Any]]) -> None:
322329

323330

324331
def _force_into_1d_numpy_array(arr: np.ndarray, np_type, flatten: bool) -> np.ndarray:
332+
"""Ensures that the given NumPy array is one-dimensional and matches the
333+
given NumPy type, avoiding copies during the conversion if possible.
334+
"""
325335
if len(arr.shape) != 1:
326336
if flatten:
327-
arr = arr.reshape((-1,))
337+
arr = arr.reshape(-1)
328338
else:
329339
raise TypeError("NumPy array must be one-dimensional")
330340
return np.ravel(arr.astype(np_type, order="C", casting="safe", copy=False))
331341

332342

333343
def _force_into_2d_numpy_array(arr: np.ndarray, np_type) -> np.ndarray:
344+
"""Ensures that the given NumPy array is two-dimensional and matches the
345+
given NumPy type, avoiding copies during the conversion if possible.
346+
"""
334347
if len(arr.shape) != 2:
335348
raise TypeError("NumPy array must be two-dimensional")
336349
return arr.astype(np_type, order="C", casting="safe", copy=False)
@@ -365,31 +378,69 @@ def numpy_array_to_igraph_vector_bool_t(
365378
) -> _VectorBool:
366379
"""Converts a one-dimensional NumPy array to an igraph vector of booleans."""
367380
arr = _force_into_1d_numpy_array(arr, np_type_of_igraph_bool_t, flatten=flatten)
368-
arr = np.ravel(
369-
arr.astype(np_type_of_igraph_bool_t, order="C", casting="safe", copy=False)
370-
)
371381
return _VectorBool.create_with(
372382
igraph_vector_bool_init_array,
373383
arr.ctypes.data_as(POINTER(igraph_bool_t)),
374384
arr.shape[0],
375385
)
376386

377387

388+
def numpy_array_to_igraph_vector_bool_t_view(
389+
arr: np.ndarray, flatten: bool = False
390+
) -> _VectorBool:
391+
"""Provides a view into an existing one-dimensional NumPy array with an
392+
igraph boolean vector view if the data type and the layout of the NumPy
393+
array is suitable. If the NumPy array is not suitable, it will be copied
394+
into the appropriate layout and data type first and then a view will be
395+
provided into the copy.
396+
"""
397+
arr = _force_into_1d_numpy_array(arr, np_type_of_igraph_real_t, flatten=flatten)
398+
399+
result = _VectorBool()
400+
igraph_vector_bool_view(
401+
result, arr.ctypes.data_as(POINTER(igraph_bool_t)), arr.shape[0]
402+
)
403+
404+
# Destructor must not be called so we never mark result as initialized;
405+
# this is intentional
406+
407+
return result
408+
409+
378410
def numpy_array_to_igraph_vector_int_t(
379411
arr: np.ndarray, flatten: bool = False
380412
) -> _VectorInt:
381413
"""Converts a one-dimensional NumPy array to an igraph vector of integers."""
382414
arr = _force_into_1d_numpy_array(arr, np_type_of_igraph_integer_t, flatten=flatten)
383-
arr = np.ravel(
384-
arr.astype(np_type_of_igraph_integer_t, order="C", casting="safe", copy=False)
385-
)
386415
return _VectorInt.create_with(
387416
igraph_vector_int_init_array,
388417
arr.ctypes.data_as(POINTER(igraph_integer_t)),
389418
arr.shape[0],
390419
)
391420

392421

422+
def numpy_array_to_igraph_vector_int_t_view(
423+
arr: np.ndarray, flatten: bool = False
424+
) -> _VectorInt:
425+
"""Provides a view into an existing one-dimensional NumPy array with an
426+
igraph integer vector view if the data type and the layout of the NumPy
427+
array is suitable. If the NumPy array is not suitable, it will be copied
428+
into the appropriate layout and data type first and then a view will be
429+
provided into the copy.
430+
"""
431+
arr = _force_into_1d_numpy_array(arr, np_type_of_igraph_integer_t, flatten=flatten)
432+
433+
result = _VectorInt()
434+
igraph_vector_int_view(
435+
result, arr.ctypes.data_as(POINTER(igraph_integer_t)), arr.shape[0]
436+
)
437+
438+
# Destructor must not be called so we never mark result as initialized;
439+
# this is intentional
440+
441+
return result
442+
443+
393444
def numpy_array_to_igraph_vector_t(arr: np.ndarray, flatten: bool = False) -> _Vector:
394445
"""Converts a one-dimensional NumPy array to an igraph vector of reals."""
395446
arr = _force_into_1d_numpy_array(arr, np_type_of_igraph_real_t, flatten=flatten)
@@ -400,6 +451,26 @@ def numpy_array_to_igraph_vector_t(arr: np.ndarray, flatten: bool = False) -> _V
400451
)
401452

402453

454+
def numpy_array_to_igraph_vector_t_view(
455+
arr: np.ndarray, flatten: bool = False
456+
) -> _Vector:
457+
"""Provides a view into an existing one-dimensional NumPy array with an
458+
igraph floating-point vector view if the data type and the layout of the NumPy
459+
array is suitable. If the NumPy array is not suitable, it will be copied
460+
into the appropriate layout and data type first and then a view will be
461+
provided into the copy.
462+
"""
463+
arr = _force_into_1d_numpy_array(arr, np_type_of_igraph_real_t, flatten=flatten)
464+
465+
result = _Vector()
466+
igraph_vector_view(result, arr.ctypes.data_as(POINTER(igraph_real_t)), arr.shape[0])
467+
468+
# Destructor must not be called so we never mark result as initialized;
469+
# this is intentional
470+
471+
return result
472+
473+
403474
def vertexlike_to_igraph_integer_t(vertex: VertexLike) -> igraph_integer_t:
404475
"""Converts a vertex-like object to an igraph integer."""
405476
if isinstance(vertex, int) and vertex >= 0:

src/igraph_ctypes/_internal/functions.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2366,7 +2366,23 @@ def topological_sorting(graph: _Graph, mode: NeighborMode = NeighborMode.OUT) ->
23662366
# Construct return value
23672367
return res
23682368

2369-
# igraph_feedback_arc_set: no Python type known for type: FAS_ALGORITHM
2369+
2370+
def feedback_arc_set(graph: _Graph, weights: Optional[Iterable[float]] = None, algo: FeedbackArcSetAlgorithm = FeedbackArcSetAlgorithm.APPROX_EADES) -> npt.NDArray[np_type_of_igraph_integer_t]:
2371+
"""Type-annotated wrapper for ``igraph_feedback_arc_set``."""
2372+
# Prepare input arguments
2373+
c_graph = graph
2374+
c_result = _VectorInt.create(0)
2375+
c_weights = edge_weights_to_igraph_vector_t_view(weights, graph)
2376+
c_algo = c_int(algo)
2377+
2378+
# Call wrapped function
2379+
igraph_feedback_arc_set(c_graph, c_result, c_weights, c_algo)
2380+
2381+
# Prepare output arguments
2382+
result = igraph_vector_int_t_to_numpy_array(c_result)
2383+
2384+
# Construct return value
2385+
return result
23702386

23712387

23722388
def is_loop(graph: _Graph, es: EdgeSelector = "all") -> npt.NDArray[np_type_of_igraph_bool_t]:

src/igraph_ctypes/_internal/lib.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def _load_igraph_c_library():
7575
igraph_vector_init_array.argtypes = [POINTER(igraph_vector_t), POINTER(igraph_real_t), igraph_integer_t]
7676

7777
igraph_vector_view = _lib.igraph_vector_view
78-
igraph_vector_view.restype = handle_igraph_error_t
78+
igraph_vector_view.restype = POINTER(igraph_vector_t)
7979
igraph_vector_view.argtypes = [POINTER(igraph_vector_t), POINTER(igraph_real_t), igraph_integer_t]
8080

8181
igraph_vector_e = _lib.igraph_vector_e
@@ -109,7 +109,7 @@ def _load_igraph_c_library():
109109
igraph_vector_int_init_array.argtypes = [POINTER(igraph_vector_int_t), POINTER(igraph_integer_t), igraph_integer_t]
110110

111111
igraph_vector_int_view = _lib.igraph_vector_int_view
112-
igraph_vector_int_view.restype = handle_igraph_error_t
112+
igraph_vector_int_view.restype = POINTER(igraph_vector_int_t)
113113
igraph_vector_int_view.argtypes = [POINTER(igraph_vector_int_t), POINTER(igraph_integer_t), igraph_integer_t]
114114

115115
igraph_vector_int_e = _lib.igraph_vector_int_e
@@ -143,7 +143,7 @@ def _load_igraph_c_library():
143143
igraph_vector_bool_init_array.argtypes = [POINTER(igraph_vector_bool_t), POINTER(igraph_bool_t), igraph_integer_t]
144144

145145
igraph_vector_bool_view = _lib.igraph_vector_bool_view
146-
igraph_vector_bool_view.restype = handle_igraph_error_t
146+
igraph_vector_bool_view.restype = POINTER(igraph_vector_bool_t)
147147
igraph_vector_bool_view.argtypes = [POINTER(igraph_vector_bool_t), POINTER(igraph_bool_t), igraph_integer_t]
148148

149149
igraph_vector_bool_e = _lib.igraph_vector_bool_e
@@ -187,7 +187,7 @@ def _load_igraph_c_library():
187187
]
188188

189189
igraph_matrix_view = _lib.igraph_matrix_view
190-
igraph_matrix_view.restype = handle_igraph_error_t
190+
igraph_matrix_view.restype = POINTER(igraph_matrix_t)
191191
igraph_matrix_view.argtypes = [
192192
POINTER(igraph_matrix_t),
193193
POINTER(igraph_real_t),
@@ -236,7 +236,7 @@ def _load_igraph_c_library():
236236
]
237237

238238
igraph_matrix_int_view = _lib.igraph_matrix_int_view
239-
igraph_matrix_int_view.restype = handle_igraph_error_t
239+
igraph_matrix_int_view.restype = POINTER(igraph_matrix_int_t)
240240
igraph_matrix_int_view.argtypes = [
241241
POINTER(igraph_matrix_int_t),
242242
POINTER(igraph_integer_t),

src/igraph_ctypes/constructors.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1+
from typing import Iterable
2+
13
from .graph import Graph
4+
from ._internal.functions import create
5+
6+
__all__ = ("create_empty_graph", "create_graph_from_edge_list")
27

38

49
def create_empty_graph(n: int, directed: bool = False) -> Graph:
@@ -9,3 +14,16 @@ def create_empty_graph(n: int, directed: bool = False) -> Graph:
914
directed: whether the graph is directed
1015
"""
1116
return Graph(n, directed)
17+
18+
19+
def create_graph_from_edge_list(
20+
edges: Iterable[int], n: int = 0, directed: bool = False
21+
) -> Graph:
22+
"""Creates a graph from the given edge list.
23+
24+
Parameters:
25+
edges: the list of edges in the graph
26+
n: the number of vertices in the graph if it cannot be inferred from
27+
the maximum edge ID in the edge list
28+
"""
29+
return Graph(_wrap=create(edges, n, directed))

src/igraph_ctypes/graph.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
edge,
1212
empty,
1313
get_eid,
14+
incident,
1415
is_directed,
1516
neighbors,
1617
vcount,
@@ -83,6 +84,11 @@ def get_edge_id(
8384
"""
8485
return get_eid(self._instance, from_, to, directed, error)
8586

87+
def incident(
88+
self, vid: VertexLike, mode: NeighborMode = NeighborMode.ALL
89+
) -> NDArray[np_type_of_igraph_integer_t]:
90+
return incident(self._instance, vid, mode)
91+
8692
def is_directed(self) -> bool:
8793
"""Returns whether the graph is directed."""
8894
return is_directed(self._instance)

0 commit comments

Comments
 (0)