Skip to content

Commit 98cfe7d

Browse files
committed
feat: attributes updated when vertices or edges are deleted; added igraph_full()
1 parent f2d0e2c commit 98cfe7d

File tree

8 files changed

+151
-30
lines changed

8 files changed

+151
-30
lines changed

docs/fragments/igraph_full.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Creates a full graph with the given number of vertices.
2+
3+
Parameters:
4+
n: the number of vertices
5+
directed: whether the graph is directed
6+
loops: whether each vertex should also have a loop edge
7+
8+
Returns:
9+
the newly created graph

src/igraph_ctypes/_internal/attributes/handler.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

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

@@ -10,6 +10,7 @@
1010
numpy_array_to_igraph_vector_t_view,
1111
)
1212
from igraph_ctypes._internal.enums import AttributeElementType, AttributeType
13+
from igraph_ctypes._internal.functions import igraph_ecount, igraph_vcount
1314
from igraph_ctypes._internal.lib import (
1415
igraph_error,
1516
igraph_es_as_vector,
@@ -27,7 +28,10 @@
2728
igraph_strvector_push_back,
2829
igraph_strvector_set,
2930
)
30-
from igraph_ctypes._internal.types import igraph_attribute_table_t, igraph_bool_t
31+
from igraph_ctypes._internal.types import (
32+
igraph_attribute_table_t,
33+
igraph_bool_t,
34+
)
3135
from igraph_ctypes._internal.utils import nop, protect_with, protect_with_default
3236

3337
from .storage import (
@@ -59,6 +63,10 @@ def _trigger_error(error: int) -> int:
5963
)
6064

6165

66+
def _are_pointers_equal(foo, bar):
67+
return cast(foo, c_void_p).value == cast(bar, c_void_p).value
68+
69+
6270
class AttributeHandlerBase:
6371
"""Base class for igraph attribute handlers."""
6472

@@ -122,9 +130,18 @@ def copy(
122130
copy_edge_attributes: bool,
123131
):
124132
storage = get_storage_from_graph(graph)
125-
new_storage = storage.copy(
126-
copy_graph_attributes, copy_vertex_attributes, copy_edge_attributes
127-
)
133+
134+
if copy_vertex_attributes:
135+
new_vcount = -1
136+
else:
137+
new_vcount = int(igraph_vcount(to))
138+
139+
if copy_edge_attributes:
140+
new_ecount = -1
141+
else:
142+
new_ecount = int(igraph_ecount(to))
143+
144+
new_storage = storage.copy(copy_graph_attributes, new_vcount, new_ecount)
128145
assign_storage_to_graph(to, new_storage)
129146

130147
def add_vertices(self, graph, n: int, attr) -> None:
@@ -139,10 +156,17 @@ def add_vertices(self, graph, n: int, attr) -> None:
139156
get_storage_from_graph(graph).add_vertices(graph, n)
140157

141158
def permute_vertices(self, graph, to, mapping):
142-
pass
159+
mapping_array = igraph_vector_int_t_to_numpy_array_view(mapping)
160+
161+
old_attrs = get_storage_from_graph(graph).get_vertex_attribute_map()
162+
new_attrs = get_storage_from_graph(to).get_vertex_attribute_map()
163+
164+
# The code below works for graph == to and graph != to as well
165+
for name, values in old_attrs.items():
166+
new_attrs.set(name, values[mapping_array], _check_length=False)
143167

144168
def combine_vertices(self, graph, to, mapping, combinations):
145-
pass
169+
assert not _are_pointers_equal(graph, to)
146170

147171
def add_edges(self, graph, edges, attr) -> None:
148172
# attr will only ever be NULL here so raise an error if it is not
@@ -157,10 +181,17 @@ def add_edges(self, graph, edges, attr) -> None:
157181
get_storage_from_graph(graph).add_edges(graph, edge_array)
158182

159183
def permute_edges(self, graph, to, mapping):
160-
pass
184+
mapping_array = igraph_vector_int_t_to_numpy_array_view(mapping)
185+
186+
old_attrs = get_storage_from_graph(graph).get_edge_attribute_map()
187+
new_attrs = get_storage_from_graph(to).get_edge_attribute_map()
188+
189+
# The code below works for graph == to and graph != to as well
190+
for name, values in old_attrs.items():
191+
new_attrs.set(name, values[mapping_array], _check_length=False)
161192

162193
def combine_edges(self, graph, to, mapping, combinations):
163-
pass
194+
assert not _are_pointers_equal(graph, to)
164195

165196
def get_info(self, graph, gnames, gtypes, vnames, vtypes, enames, etypes):
166197
storage = get_storage_from_graph(graph)

src/igraph_ctypes/_internal/attributes/map.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,28 @@ def copy(self: C) -> C:
4242
self._common_length_of_values,
4343
)
4444

45-
def copy_empty(self: C) -> C:
46-
"""Returns another, empty attribute map with the same expected length
45+
def copy_empty(self: C, expected_length: int = -1) -> C:
46+
"""Returns another, empty attribute map with the given expected length
4747
for any items being assigned in the future.
48+
49+
Args:
50+
expected_length: the expected length for any items being assigned
51+
to the new copy in the future; negative if the expected length
52+
should be the same as for this instance
4853
"""
49-
return self.__class__.wrap_empty_dict(self._common_length_of_values)
54+
return self.__class__.wrap_empty_dict(
55+
expected_length if expected_length >= 0 else self._common_length_of_values
56+
)
5057

5158
def remove(self, key: str) -> None:
5259
del self._items[key]
5360

54-
def set(self, key: str, value: T | Iterable[T]) -> None:
61+
def set(
62+
self,
63+
key: str,
64+
value: T | Iterable[T],
65+
_check_length: bool = True,
66+
) -> None:
5567
"""Assigns a value to _all_ the attribute values corresponding to the
5668
given attribute.
5769
@@ -79,7 +91,7 @@ def set(self, key: str, value: T | Iterable[T]) -> None:
7991
# iterables are mapped to an AttributeValueList. Note that this
8092
# also takes care of copying existing AttributeValueList instances
8193
avl = AttributeValueList(value, fixed_length=True) # type: ignore
82-
if len(avl) != length:
94+
if _check_length and len(avl) != length:
8395
raise RuntimeError(
8496
f"attribute value list length must be {length}, got {len(avl)}"
8597
)
@@ -90,7 +102,9 @@ def set(self, key: str, value: T | Iterable[T]) -> None:
90102
[value] * length, fixed_length=True
91103
) # type: ignore
92104

93-
assert avl.fixed_length and len(avl) == length
105+
assert avl.fixed_length
106+
assert not _check_length or len(avl) == length
107+
94108
self._items[key] = avl
95109

96110
def _extend_common_length(self, n: int) -> None:

src/igraph_ctypes/_internal/attributes/storage.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from abc import ABC, abstractmethod
22
from ctypes import py_object
33
from dataclasses import dataclass, field
4-
from typing import Any, MutableMapping, Optional
4+
from typing import Any, MutableMapping, Optional, TypeVar
55

66
from igraph_ctypes._internal.refcount import incref, decref
77
from igraph_ctypes._internal.types import IntArray
@@ -17,6 +17,9 @@
1717
)
1818

1919

20+
C = TypeVar("C", bound="AttributeStorage")
21+
22+
2023
class AttributeStorage(ABC):
2124
"""Interface specification for objects that store graph, vertex and edge
2225
attributes.
@@ -43,11 +46,11 @@ def clear(self):
4346

4447
@abstractmethod
4548
def copy(
46-
self,
49+
self: C,
4750
copy_graph_attributes: bool = True,
48-
copy_vertex_attributes: bool = True,
49-
copy_edge_attributes: bool = True,
50-
):
51+
new_vcount: int = -1,
52+
new_ecount: int = -1,
53+
) -> C:
5154
"""Creates a shallow copy of the storage area."""
5255
raise NotImplementedError
5356

@@ -101,17 +104,17 @@ def clear(self) -> None:
101104
def copy(
102105
self,
103106
copy_graph_attributes: bool = True,
104-
copy_vertex_attributes: bool = True,
105-
copy_edge_attributes: bool = True,
107+
new_vertex_count: int = -1,
108+
new_edge_count: int = -1,
106109
):
107110
return self.__class__(
108111
self.graph_attributes.copy() if copy_graph_attributes else {},
109112
self.vertex_attributes.copy()
110-
if copy_vertex_attributes
111-
else self.vertex_attributes.copy_empty(),
113+
if new_vertex_count < 0
114+
else self.vertex_attributes.copy_empty(new_vertex_count),
112115
self.edge_attributes.copy()
113-
if copy_edge_attributes
114-
else self.edge_attributes.copy_empty(),
116+
if new_edge_count < 0
117+
else self.edge_attributes.copy_empty(new_edge_count),
115118
)
116119

117120
def get_graph_attribute_map(self) -> MutableMapping[str, Any]:

src/igraph_ctypes/_internal/functions.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,16 @@ def regular_tree(h: int, k: int = 3, type: TreeMode = TreeMode.UNDIRECTED) -> Gr
559559

560560

561561
def full(n: int, directed: bool = False, loops: bool = False) -> Graph:
562-
"""Type-annotated wrapper for ``igraph_full``."""
562+
"""Creates a full graph with the given number of vertices.
563+
564+
Parameters:
565+
n: the number of vertices
566+
directed: whether the graph is directed
567+
loops: whether each vertex should also have a loop edge
568+
569+
Returns:
570+
the newly created graph
571+
"""
563572
# Prepare input arguments
564573
c_graph = _Graph()
565574
c_n = n

src/igraph_ctypes/constructors.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
create as create_graph_from_edge_list,
66
empty as create_empty_graph,
77
famous as create_famous_graph,
8+
full as create_full_graph,
89
grg_game,
910
square_lattice,
1011
)
1112

1213
__all__ = (
1314
"create_empty_graph",
1415
"create_famous_graph",
16+
"create_full_graph",
1517
"create_geometric_random_graph",
1618
"create_graph_from_edge_list",
1719
"create_square_lattice",

tests/test_constructors.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
from numpy import array, ndarray
44
from numpy.testing import assert_array_equal
55

6+
from igraph_ctypes.conversion import get_edge_list
67
from igraph_ctypes.constructors import (
78
create_famous_graph,
9+
create_full_graph,
810
create_graph_from_edge_list,
911
create_square_lattice,
1012
)
11-
from igraph_ctypes._internal.functions import get_edgelist
1213

1314

1415
def test_create_famous_graph():
@@ -19,6 +20,33 @@ def test_create_famous_graph():
1920
assert not g.is_directed() and g.vcount() == 10 and g.ecount() == 15
2021

2122

23+
def test_create_full_graph():
24+
g = create_full_graph(0)
25+
assert g.vcount() == 0
26+
assert g.ecount() == 0
27+
assert not g.is_directed()
28+
29+
g = create_full_graph(1, directed=True)
30+
assert g.vcount() == 1
31+
assert g.ecount() == 0
32+
assert g.is_directed()
33+
34+
g = create_full_graph(5, loops=True)
35+
assert g.vcount() == 5
36+
assert g.ecount() == 15
37+
assert not g.is_directed()
38+
# fmt: off
39+
assert_array_equal(
40+
get_edge_list(g).reshape(-1),
41+
array([0, 0, 0, 1, 0, 2, 0, 3, 0, 4,
42+
1, 1, 1, 2, 1, 3, 1, 4,
43+
2, 2, 2, 3, 2, 4,
44+
3, 3, 3, 4,
45+
4, 4])
46+
)
47+
# fmt: on
48+
49+
2250
@pytest.mark.parametrize(
2351
("edges", "n", "directed"),
2452
[
@@ -52,7 +80,7 @@ def test_create_square_lattice():
5280
assert g.vcount() == 12 and g.ecount() == 17
5381
# fmt: off
5482
assert_array_equal(
55-
get_edgelist(g._instance),
83+
get_edge_list(g).reshape(-1),
5684
array([0, 1, 0, 4, 1, 2, 1, 5, 2, 3, 2, 6, 3, 7,
5785
4, 5, 4, 8, 5, 6, 5, 9, 6, 7, 6, 10, 7, 11,
5886
8, 9, 9, 10, 10, 11])

tests/test_graph_attributes.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
from collections.abc import MutableMapping
2-
from igraph_ctypes.constructors import create_empty_graph, create_famous_graph
2+
from igraph_ctypes.constructors import (
3+
create_empty_graph,
4+
create_famous_graph,
5+
create_full_graph,
6+
)
37

48

59
def test_attribute_mapping_basic_operations():
@@ -37,3 +41,24 @@ def test_copying_graph_copies_attributes():
3741
del g2.attrs["age"]
3842
assert "age" not in g2.attrs
3943
assert g.attrs["age"] == 42
44+
45+
46+
def test_deleting_edges_updates_edge_attributes():
47+
g = create_full_graph(6)
48+
weights = [i + 10 for i in range(g.ecount())]
49+
g.eattrs.set("weight", weights)
50+
51+
assert list(g.eattrs["weight"]) == weights
52+
g.delete_edges([6, 4, 11])
53+
assert list(g.eattrs["weight"]) == [10, 11, 12, 13, 15, 17, 18, 19, 20, 22, 23, 24]
54+
55+
56+
def test_deleting_vertices_updates_vertex_and_edge_attributes():
57+
g = create_full_graph(5)
58+
g.vattrs.set("name", list("ABCDE"))
59+
weights = [i + 10 for i in range(g.ecount())]
60+
g.eattrs.set("weight", weights)
61+
62+
assert list(g.vattrs["name"]) == ["A", "B", "C", "D", "E"]
63+
g.delete_vertices([0, 3])
64+
assert list(g.eattrs["weight"]) == [14, 16, 18]

0 commit comments

Comments
 (0)