Skip to content

Commit d57532b

Browse files
committed
feat: first prototype of edge attribute combinations that works
1 parent 5f40400 commit d57532b

File tree

14 files changed

+395
-19
lines changed

14 files changed

+395
-19
lines changed

src/codegen/internal_lib.py.in

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ from .types import (
3434
igraph_bliss_info_t,
3535
igraph_error_handler_t,
3636
igraph_fatal_handler_t,
37-
igraph_function_pointer_t,
3837
igraph_hrg_t,
3938
igraph_layout_drl_options_t,
4039
igraph_maxflow_stats_t,
@@ -546,11 +545,11 @@ igraph_attribute_combination_destroy.argtypes = [POINTER(igraph_attribute_combin
546545

547546
igraph_attribute_combination_add = _lib.igraph_attribute_combination_add
548547
igraph_attribute_combination_add.restype = handle_igraph_error_t
549-
igraph_attribute_combination_add.argtypes = [POINTER(igraph_attribute_combination_t), c_char_p, c_int, igraph_function_pointer_t]
548+
igraph_attribute_combination_add.argtypes = [POINTER(igraph_attribute_combination_t), c_char_p, c_int, c_void_p]
550549

551550
igraph_attribute_combination_query = _lib.igraph_attribute_combination_query
552551
igraph_attribute_combination_query.restype = handle_igraph_error_t
553-
igraph_attribute_combination_query.argtypes = [POINTER(igraph_attribute_combination_t), c_char_p, POINTER(c_int), POINTER(igraph_function_pointer_t)]
552+
igraph_attribute_combination_query.argtypes = [POINTER(igraph_attribute_combination_t), c_char_p, POINTER(c_int), POINTER(c_void_p)]
554553

555554
# Error handling and interruptions
556555

src/codegen/run.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,8 @@ def main():
353353
SOURCE_FOLDER / "igraph_ctypes" / "types.py",
354354
"._internal.types",
355355
match=(
356+
"AttributeCombinationSpecification",
357+
"AttributeCombinationSpecificationEntry",
356358
"BoolArray",
357359
"EdgeLike",
358360
"EdgeSelector",
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
from ctypes import c_void_p
2+
from numpy import mean, median
3+
from typing import Any, Callable, Iterable
4+
5+
from igraph_ctypes._internal.enums import AttributeCombinationType, AttributeType
6+
from igraph_ctypes._internal.types import IntArray
7+
8+
from .value_list import AttributeValueList
9+
10+
11+
Handler = Callable[[AttributeValueList, list[IntArray], c_void_p], Iterable[Any] | None]
12+
13+
14+
def apply_attribute_combinations(
15+
values: AttributeValueList,
16+
mapping: list[IntArray],
17+
comb_type: int,
18+
comb_func: c_void_p,
19+
) -> Iterable[Any] | None:
20+
"""Applies an igraph attribute combination specification entry to a
21+
vector of attributes.
22+
23+
Args:
24+
values: an attribute value list from the old graph where the entries have
25+
to be combined into a new atribute value list
26+
mapping: list of integer arrays where the i-th entry lists the indices
27+
from the values array that are to be merged into the i-th entry of
28+
the returned value list
29+
comb_type: type of attribute combination to apply; one of the constants
30+
from the AttributeCombinationType_ enum
31+
comb_func: pointer to a Python function to invoke for custom, user-defined
32+
attribute combinations (i.e. when comb_type is equal to
33+
`AttributeCombinationType.FUNCTION`)
34+
35+
Returns:
36+
an iterable of the combined values or `None` if the values should be ignored
37+
(i.e. when `comb_type` is `AttributeCombinationType.IGNORE`)
38+
"""
39+
if comb_type == AttributeCombinationType.IGNORE:
40+
return None
41+
42+
try:
43+
handler = _handlers[comb_type]
44+
except IndexError:
45+
handler = _combine_ignore
46+
47+
return handler(values, mapping, comb_func)
48+
49+
50+
def _combine_ignore(
51+
values: AttributeValueList,
52+
mapping: list[IntArray],
53+
comb_func: c_void_p,
54+
) -> None:
55+
return None
56+
57+
58+
def _combine_with_function(
59+
values: AttributeValueList,
60+
mapping: list[IntArray],
61+
comb_func: c_void_p,
62+
) -> None:
63+
raise NotImplementedError # TODO(ntamas)
64+
65+
66+
def _combine_sum(
67+
values: AttributeValueList,
68+
mapping: list[IntArray],
69+
comb_func: c_void_p,
70+
) -> Iterable[Any]:
71+
return (values[item].sum() for item in mapping)
72+
73+
74+
def _combine_prod(
75+
values: AttributeValueList,
76+
mapping: list[IntArray],
77+
comb_func: c_void_p,
78+
) -> Iterable[Any]:
79+
return (values[item].prod() for item in mapping)
80+
81+
82+
def _combine_min(
83+
values: AttributeValueList,
84+
mapping: list[IntArray],
85+
comb_func: c_void_p,
86+
) -> Iterable[Any]:
87+
return (values[item].min() for item in mapping)
88+
89+
90+
def _combine_max(
91+
values: AttributeValueList,
92+
mapping: list[IntArray],
93+
comb_func: c_void_p,
94+
) -> Iterable[Any]:
95+
return (values[item].min() for item in mapping)
96+
97+
98+
def _combine_random(
99+
values: AttributeValueList,
100+
mapping: list[IntArray],
101+
comb_func: c_void_p,
102+
) -> Iterable[Any]:
103+
raise NotImplementedError # TODO(ntamas)
104+
105+
106+
def _combine_first(
107+
values: AttributeValueList,
108+
mapping: list[IntArray],
109+
comb_func: c_void_p,
110+
) -> Iterable[Any]:
111+
indices = [item[0] for item in mapping]
112+
return values[indices]
113+
114+
115+
def _combine_last(
116+
values: AttributeValueList,
117+
mapping: list[IntArray],
118+
comb_func: c_void_p,
119+
) -> Iterable[Any]:
120+
indices = [item[-1] for item in mapping]
121+
return values[indices]
122+
123+
124+
def _combine_mean(
125+
values: AttributeValueList,
126+
mapping: list[IntArray],
127+
comb_func: c_void_p,
128+
) -> Iterable[Any]:
129+
return (mean(values[item]) for item in mapping)
130+
131+
132+
def _combine_median(
133+
values: AttributeValueList,
134+
mapping: list[IntArray],
135+
comb_func: c_void_p,
136+
) -> Iterable[Any]:
137+
return (median(values[item]) for item in mapping)
138+
139+
140+
def _combine_concat(
141+
values: AttributeValueList,
142+
mapping: list[IntArray],
143+
comb_func: c_void_p,
144+
) -> Iterable[Any]:
145+
if values.type != AttributeType.STRING:
146+
raise TypeError(f"cannot concatenate attributes of type {values.type}")
147+
return ("".join(values[item]) for item in mapping)
148+
149+
150+
_handlers: list[Handler] = [
151+
_combine_ignore,
152+
_combine_first,
153+
_combine_with_function,
154+
_combine_sum,
155+
_combine_prod,
156+
_combine_min,
157+
_combine_max,
158+
_combine_random,
159+
_combine_first,
160+
_combine_last,
161+
_combine_mean,
162+
_combine_median,
163+
_combine_concat,
164+
]
165+
"""Table of attribute combination handler functions."""

src/igraph_ctypes/_internal/attributes/handler.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
from __future__ import annotations
22

3-
from ctypes import cast, pointer, c_void_p
3+
from ctypes import cast, c_int, pointer, c_void_p
44
from math import nan
55
from typing import Any, Callable, Optional, TYPE_CHECKING
66

77
from igraph_ctypes._internal.conversion import (
88
igraph_vector_int_t_to_numpy_array_view,
9+
igraph_vector_int_list_t_to_list_of_numpy_array_view,
910
numpy_array_to_igraph_vector_bool_t_view,
1011
numpy_array_to_igraph_vector_t_view,
1112
)
1213
from igraph_ctypes._internal.enums import AttributeElementType, AttributeType
1314
from igraph_ctypes._internal.functions import igraph_ecount, igraph_vcount
1415
from igraph_ctypes._internal.lib import (
16+
igraph_attribute_combination_query,
1517
igraph_error,
1618
igraph_es_as_vector,
1719
igraph_vector_resize,
@@ -34,6 +36,7 @@
3436
)
3537
from igraph_ctypes._internal.utils import nop, protect_with, protect_with_default
3638

39+
from .combinations import apply_attribute_combinations
3740
from .storage import (
3841
DictAttributeStorage,
3942
assign_storage_to_graph,
@@ -193,6 +196,28 @@ def permute_edges(self, graph, to, mapping):
193196
def combine_edges(self, graph, to, mapping, combinations):
194197
assert not _are_pointers_equal(graph, to)
195198

199+
mapping_arrays = igraph_vector_int_list_t_to_list_of_numpy_array_view(mapping)
200+
201+
old_attrs = get_storage_from_graph(graph).get_edge_attribute_map()
202+
new_attrs = get_storage_from_graph(to).get_edge_attribute_map()
203+
204+
for name, values in old_attrs.items():
205+
comb_type = c_int()
206+
comb_func = c_void_p()
207+
208+
igraph_attribute_combination_query(
209+
combinations,
210+
name.encode("utf-8"),
211+
comb_type,
212+
comb_func,
213+
)
214+
215+
new_values = apply_attribute_combinations(
216+
values, mapping_arrays, comb_type.value, comb_func
217+
)
218+
if new_values is not None:
219+
new_attrs.set(name, new_values, type=values.type, _check_length=False)
220+
196221
def get_info(self, graph, gnames, gtypes, vnames, vtypes, enames, etypes):
197222
storage = get_storage_from_graph(graph)
198223

src/igraph_ctypes/_internal/attributes/map.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from collections.abc import MutableMapping
22
from typing import Iterable, Iterator, TypeVar
33

4+
from .enums import AttributeType
45
from .value_list import AttributeValueList
56

67
__all__ = ("AttributeMap",)
@@ -62,6 +63,7 @@ def set(
6263
self,
6364
key: str,
6465
value: T | Iterable[T],
66+
type: AttributeType | None = None,
6567
_check_length: bool = True,
6668
) -> None:
6769
"""Assigns a value to _all_ the attribute values corresponding to the
@@ -85,12 +87,12 @@ def set(
8587
if isinstance(value, (bytes, str)):
8688
# strings and bytes are iterable but they are treated as if not
8789
avl = AttributeValueList(
88-
[value] * length, fixed_length=True
90+
[value] * length, type=type, fixed_length=True
8991
) # type: ignore
9092
elif isinstance(value, Iterable):
9193
# iterables are mapped to an AttributeValueList. Note that this
9294
# also takes care of copying existing AttributeValueList instances
93-
avl = AttributeValueList(value, fixed_length=True) # type: ignore
95+
avl = AttributeValueList(value, type=type, fixed_length=True) # type: ignore
9496
if _check_length and len(avl) != length:
9597
raise RuntimeError(
9698
f"attribute value list length must be {length}, got {len(avl)}"
@@ -99,7 +101,7 @@ def set(
99101
# all other values are assumed to be a common value for all
100102
# vertices or edges
101103
avl = AttributeValueList(
102-
[value] * length, fixed_length=True
104+
[value] * length, type=type, fixed_length=True
103105
) # type: ignore
104106

105107
assert avl.fixed_length

src/igraph_ctypes/_internal/attributes/value_list.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import numpy as np
22

33
from math import ceil
4-
from numpy.typing import NDArray
4+
from numpy.typing import DTypeLike, NDArray
55
from types import EllipsisType
66
from typing import (
77
Any,
@@ -112,7 +112,23 @@ def __init__(
112112
fixed_length: bool = False,
113113
_wrap: bool = False,
114114
):
115-
"""Constructor."""
115+
"""Constructor.
116+
117+
Creates a new attribute value list from an iterable of items.
118+
119+
The input should typically be a list, tuple, NumPy array or any other
120+
iterable. However, if you have an iterable on which you can iterate
121+
only _once_, you also need to specify the igraph attribute type
122+
explicitly. This is because the constructor will attempt to infer the
123+
attribute type if it is not given, but by doing so it will consume the
124+
iterable once.
125+
126+
Args:
127+
items: the iterable of items to store in the list
128+
type: the igraph attribute type of the attribute that this list
129+
will belong to
130+
fixed_length: whether the list is fixed-length
131+
"""
116132
if _wrap:
117133
# Special case, not for public use. items is a NumPy array and
118134
# we can wrap it as is. Must be called only when no one else has
@@ -188,6 +204,11 @@ def fixed_length(self) -> bool:
188204
"""Returns whether the list is fixed-length."""
189205
return self._fixed_length
190206

207+
@property
208+
def dtype(self) -> DTypeLike:
209+
"""Returns the NumPy attribute type of this list."""
210+
return igraph_to_numpy_attribute_type(self._type)
211+
191212
@property
192213
def type(self) -> AttributeType:
193214
"""Returns the igraph attribute type of this list."""

0 commit comments

Comments
 (0)