exonware-xwnode 0.0.1.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- exonware/__init__.py +14 -0
- exonware/xwnode/__init__.py +127 -0
- exonware/xwnode/base.py +676 -0
- exonware/xwnode/config.py +178 -0
- exonware/xwnode/contracts.py +730 -0
- exonware/xwnode/errors.py +503 -0
- exonware/xwnode/facade.py +460 -0
- exonware/xwnode/strategies/__init__.py +158 -0
- exonware/xwnode/strategies/advisor.py +463 -0
- exonware/xwnode/strategies/edges/__init__.py +32 -0
- exonware/xwnode/strategies/edges/adj_list.py +227 -0
- exonware/xwnode/strategies/edges/adj_matrix.py +391 -0
- exonware/xwnode/strategies/edges/base.py +169 -0
- exonware/xwnode/strategies/flyweight.py +328 -0
- exonware/xwnode/strategies/impls/__init__.py +13 -0
- exonware/xwnode/strategies/impls/_base_edge.py +403 -0
- exonware/xwnode/strategies/impls/_base_node.py +307 -0
- exonware/xwnode/strategies/impls/edge_adj_list.py +353 -0
- exonware/xwnode/strategies/impls/edge_adj_matrix.py +445 -0
- exonware/xwnode/strategies/impls/edge_bidir_wrapper.py +455 -0
- exonware/xwnode/strategies/impls/edge_block_adj_matrix.py +539 -0
- exonware/xwnode/strategies/impls/edge_coo.py +533 -0
- exonware/xwnode/strategies/impls/edge_csc.py +447 -0
- exonware/xwnode/strategies/impls/edge_csr.py +492 -0
- exonware/xwnode/strategies/impls/edge_dynamic_adj_list.py +503 -0
- exonware/xwnode/strategies/impls/edge_flow_network.py +555 -0
- exonware/xwnode/strategies/impls/edge_hyperedge_set.py +516 -0
- exonware/xwnode/strategies/impls/edge_neural_graph.py +650 -0
- exonware/xwnode/strategies/impls/edge_octree.py +574 -0
- exonware/xwnode/strategies/impls/edge_property_store.py +655 -0
- exonware/xwnode/strategies/impls/edge_quadtree.py +519 -0
- exonware/xwnode/strategies/impls/edge_rtree.py +820 -0
- exonware/xwnode/strategies/impls/edge_temporal_edgeset.py +558 -0
- exonware/xwnode/strategies/impls/edge_tree_graph_basic.py +271 -0
- exonware/xwnode/strategies/impls/edge_weighted_graph.py +411 -0
- exonware/xwnode/strategies/manager.py +775 -0
- exonware/xwnode/strategies/metrics.py +538 -0
- exonware/xwnode/strategies/migration.py +432 -0
- exonware/xwnode/strategies/nodes/__init__.py +50 -0
- exonware/xwnode/strategies/nodes/_base_node.py +307 -0
- exonware/xwnode/strategies/nodes/adjacency_list.py +267 -0
- exonware/xwnode/strategies/nodes/aho_corasick.py +345 -0
- exonware/xwnode/strategies/nodes/array_list.py +209 -0
- exonware/xwnode/strategies/nodes/base.py +247 -0
- exonware/xwnode/strategies/nodes/deque.py +200 -0
- exonware/xwnode/strategies/nodes/hash_map.py +135 -0
- exonware/xwnode/strategies/nodes/heap.py +307 -0
- exonware/xwnode/strategies/nodes/linked_list.py +232 -0
- exonware/xwnode/strategies/nodes/node_aho_corasick.py +520 -0
- exonware/xwnode/strategies/nodes/node_array_list.py +175 -0
- exonware/xwnode/strategies/nodes/node_avl_tree.py +371 -0
- exonware/xwnode/strategies/nodes/node_b_plus_tree.py +542 -0
- exonware/xwnode/strategies/nodes/node_bitmap.py +420 -0
- exonware/xwnode/strategies/nodes/node_bitset_dynamic.py +513 -0
- exonware/xwnode/strategies/nodes/node_bloom_filter.py +347 -0
- exonware/xwnode/strategies/nodes/node_btree.py +357 -0
- exonware/xwnode/strategies/nodes/node_count_min_sketch.py +470 -0
- exonware/xwnode/strategies/nodes/node_cow_tree.py +473 -0
- exonware/xwnode/strategies/nodes/node_cuckoo_hash.py +392 -0
- exonware/xwnode/strategies/nodes/node_fenwick_tree.py +301 -0
- exonware/xwnode/strategies/nodes/node_hash_map.py +269 -0
- exonware/xwnode/strategies/nodes/node_heap.py +191 -0
- exonware/xwnode/strategies/nodes/node_hyperloglog.py +407 -0
- exonware/xwnode/strategies/nodes/node_linked_list.py +409 -0
- exonware/xwnode/strategies/nodes/node_lsm_tree.py +400 -0
- exonware/xwnode/strategies/nodes/node_ordered_map.py +390 -0
- exonware/xwnode/strategies/nodes/node_ordered_map_balanced.py +565 -0
- exonware/xwnode/strategies/nodes/node_patricia.py +512 -0
- exonware/xwnode/strategies/nodes/node_persistent_tree.py +378 -0
- exonware/xwnode/strategies/nodes/node_radix_trie.py +452 -0
- exonware/xwnode/strategies/nodes/node_red_black_tree.py +497 -0
- exonware/xwnode/strategies/nodes/node_roaring_bitmap.py +570 -0
- exonware/xwnode/strategies/nodes/node_segment_tree.py +289 -0
- exonware/xwnode/strategies/nodes/node_set_hash.py +354 -0
- exonware/xwnode/strategies/nodes/node_set_tree.py +480 -0
- exonware/xwnode/strategies/nodes/node_skip_list.py +316 -0
- exonware/xwnode/strategies/nodes/node_splay_tree.py +393 -0
- exonware/xwnode/strategies/nodes/node_suffix_array.py +487 -0
- exonware/xwnode/strategies/nodes/node_treap.py +387 -0
- exonware/xwnode/strategies/nodes/node_tree_graph_hybrid.py +1434 -0
- exonware/xwnode/strategies/nodes/node_trie.py +252 -0
- exonware/xwnode/strategies/nodes/node_union_find.py +187 -0
- exonware/xwnode/strategies/nodes/node_xdata_optimized.py +369 -0
- exonware/xwnode/strategies/nodes/priority_queue.py +209 -0
- exonware/xwnode/strategies/nodes/queue.py +161 -0
- exonware/xwnode/strategies/nodes/sparse_matrix.py +206 -0
- exonware/xwnode/strategies/nodes/stack.py +152 -0
- exonware/xwnode/strategies/nodes/trie.py +274 -0
- exonware/xwnode/strategies/nodes/union_find.py +283 -0
- exonware/xwnode/strategies/pattern_detector.py +603 -0
- exonware/xwnode/strategies/performance_monitor.py +487 -0
- exonware/xwnode/strategies/queries/__init__.py +24 -0
- exonware/xwnode/strategies/queries/base.py +236 -0
- exonware/xwnode/strategies/queries/cql.py +201 -0
- exonware/xwnode/strategies/queries/cypher.py +181 -0
- exonware/xwnode/strategies/queries/datalog.py +70 -0
- exonware/xwnode/strategies/queries/elastic_dsl.py +70 -0
- exonware/xwnode/strategies/queries/eql.py +70 -0
- exonware/xwnode/strategies/queries/flux.py +70 -0
- exonware/xwnode/strategies/queries/gql.py +70 -0
- exonware/xwnode/strategies/queries/graphql.py +240 -0
- exonware/xwnode/strategies/queries/gremlin.py +181 -0
- exonware/xwnode/strategies/queries/hiveql.py +214 -0
- exonware/xwnode/strategies/queries/hql.py +70 -0
- exonware/xwnode/strategies/queries/jmespath.py +219 -0
- exonware/xwnode/strategies/queries/jq.py +66 -0
- exonware/xwnode/strategies/queries/json_query.py +66 -0
- exonware/xwnode/strategies/queries/jsoniq.py +248 -0
- exonware/xwnode/strategies/queries/kql.py +70 -0
- exonware/xwnode/strategies/queries/linq.py +238 -0
- exonware/xwnode/strategies/queries/logql.py +70 -0
- exonware/xwnode/strategies/queries/mql.py +68 -0
- exonware/xwnode/strategies/queries/n1ql.py +210 -0
- exonware/xwnode/strategies/queries/partiql.py +70 -0
- exonware/xwnode/strategies/queries/pig.py +215 -0
- exonware/xwnode/strategies/queries/promql.py +70 -0
- exonware/xwnode/strategies/queries/sparql.py +220 -0
- exonware/xwnode/strategies/queries/sql.py +275 -0
- exonware/xwnode/strategies/queries/xml_query.py +66 -0
- exonware/xwnode/strategies/queries/xpath.py +223 -0
- exonware/xwnode/strategies/queries/xquery.py +258 -0
- exonware/xwnode/strategies/queries/xwnode_executor.py +332 -0
- exonware/xwnode/strategies/queries/xwquery_strategy.py +424 -0
- exonware/xwnode/strategies/registry.py +604 -0
- exonware/xwnode/strategies/simple.py +273 -0
- exonware/xwnode/strategies/utils.py +532 -0
- exonware/xwnode/types.py +912 -0
- exonware/xwnode/version.py +78 -0
- exonware_xwnode-0.0.1.12.dist-info/METADATA +169 -0
- exonware_xwnode-0.0.1.12.dist-info/RECORD +132 -0
- exonware_xwnode-0.0.1.12.dist-info/WHEEL +4 -0
- exonware_xwnode-0.0.1.12.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,492 @@
|
|
1
|
+
"""
|
2
|
+
Compressed Sparse Row (CSR) Edge Strategy Implementation
|
3
|
+
|
4
|
+
This module implements the CSR strategy for memory-efficient sparse graph
|
5
|
+
representation with fast row-wise operations.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from typing import Any, Iterator, Dict, List, Set, Optional, Tuple, Union
|
9
|
+
import bisect
|
10
|
+
from ._base_edge import aEdgeStrategy
|
11
|
+
from ...types import EdgeMode, EdgeTrait
|
12
|
+
|
13
|
+
|
14
|
+
class xCSRStrategy(aEdgeStrategy):
|
15
|
+
"""
|
16
|
+
Compressed Sparse Row edge strategy for memory-efficient sparse graphs.
|
17
|
+
|
18
|
+
Provides memory-efficient storage and fast row-wise operations,
|
19
|
+
ideal for large sparse graphs with infrequent edge modifications.
|
20
|
+
"""
|
21
|
+
|
22
|
+
def __init__(self, traits: EdgeTrait = EdgeTrait.NONE, **options):
|
23
|
+
"""Initialize the CSR strategy."""
|
24
|
+
super().__init__(EdgeMode.CSR, traits, **options)
|
25
|
+
|
26
|
+
self.is_directed = options.get('directed', True)
|
27
|
+
self.allow_self_loops = options.get('self_loops', True)
|
28
|
+
|
29
|
+
# CSR storage format
|
30
|
+
self._row_ptr: List[int] = [0] # Pointers to start of each row
|
31
|
+
self._col_indices: List[int] = [] # Column indices of non-zero elements
|
32
|
+
self._values: List[Dict[str, Any]] = [] # Edge data for each non-zero element
|
33
|
+
|
34
|
+
# Vertex management
|
35
|
+
self._vertex_to_index: Dict[str, int] = {}
|
36
|
+
self._index_to_vertex: Dict[int, str] = {}
|
37
|
+
self._vertex_count = 0
|
38
|
+
self._edge_count = 0
|
39
|
+
self._edge_id_counter = 0
|
40
|
+
|
41
|
+
# Build cache
|
42
|
+
self._needs_rebuild = False
|
43
|
+
self._build_cache: List[Tuple[str, str, Dict[str, Any]]] = []
|
44
|
+
|
45
|
+
def get_supported_traits(self) -> EdgeTrait:
|
46
|
+
"""Get the traits supported by the CSR strategy."""
|
47
|
+
return (EdgeTrait.SPARSE | EdgeTrait.COMPRESSED | EdgeTrait.CACHE_FRIENDLY | EdgeTrait.COLUMNAR)
|
48
|
+
|
49
|
+
# ============================================================================
|
50
|
+
# VERTEX MANAGEMENT
|
51
|
+
# ============================================================================
|
52
|
+
|
53
|
+
def _get_vertex_index(self, vertex: str) -> int:
|
54
|
+
"""Get or create index for vertex."""
|
55
|
+
if vertex in self._vertex_to_index:
|
56
|
+
return self._vertex_to_index[vertex]
|
57
|
+
|
58
|
+
# Add new vertex
|
59
|
+
index = self._vertex_count
|
60
|
+
self._vertex_to_index[vertex] = index
|
61
|
+
self._index_to_vertex[index] = vertex
|
62
|
+
self._vertex_count += 1
|
63
|
+
self._needs_rebuild = True
|
64
|
+
|
65
|
+
return index
|
66
|
+
|
67
|
+
def _rebuild_csr(self) -> None:
|
68
|
+
"""Rebuild CSR format from edge cache."""
|
69
|
+
if not self._needs_rebuild:
|
70
|
+
return
|
71
|
+
|
72
|
+
# Sort edges by source vertex index
|
73
|
+
edges_by_source = {}
|
74
|
+
for source, target, edge_data in self._build_cache:
|
75
|
+
source_idx = self._vertex_to_index[source]
|
76
|
+
target_idx = self._vertex_to_index[target]
|
77
|
+
|
78
|
+
if source_idx not in edges_by_source:
|
79
|
+
edges_by_source[source_idx] = []
|
80
|
+
edges_by_source[source_idx].append((target_idx, edge_data))
|
81
|
+
|
82
|
+
# Sort edges within each source by target index
|
83
|
+
for source_idx in edges_by_source:
|
84
|
+
edges_by_source[source_idx].sort(key=lambda x: x[0])
|
85
|
+
|
86
|
+
# Rebuild CSR arrays
|
87
|
+
self._row_ptr = [0]
|
88
|
+
self._col_indices = []
|
89
|
+
self._values = []
|
90
|
+
|
91
|
+
for source_idx in range(self._vertex_count):
|
92
|
+
if source_idx in edges_by_source:
|
93
|
+
for target_idx, edge_data in edges_by_source[source_idx]:
|
94
|
+
self._col_indices.append(target_idx)
|
95
|
+
self._values.append(edge_data)
|
96
|
+
|
97
|
+
self._row_ptr.append(len(self._col_indices))
|
98
|
+
|
99
|
+
self._build_cache.clear()
|
100
|
+
self._needs_rebuild = False
|
101
|
+
|
102
|
+
# ============================================================================
|
103
|
+
# CORE EDGE OPERATIONS
|
104
|
+
# ============================================================================
|
105
|
+
|
106
|
+
def add_edge(self, source: str, target: str, **properties) -> str:
|
107
|
+
"""Add an edge between source and target vertices."""
|
108
|
+
# Validate self-loops
|
109
|
+
if source == target and not self.allow_self_loops:
|
110
|
+
raise ValueError(f"Self-loops not allowed: {source} -> {target}")
|
111
|
+
|
112
|
+
# Get vertex indices (creates vertices if needed)
|
113
|
+
source_idx = self._get_vertex_index(source)
|
114
|
+
target_idx = self._get_vertex_index(target)
|
115
|
+
|
116
|
+
# Generate edge ID
|
117
|
+
edge_id = f"edge_{self._edge_id_counter}"
|
118
|
+
self._edge_id_counter += 1
|
119
|
+
|
120
|
+
# Create edge data
|
121
|
+
edge_data = {
|
122
|
+
'id': edge_id,
|
123
|
+
'source': source,
|
124
|
+
'target': target,
|
125
|
+
'weight': properties.get('weight', 1.0),
|
126
|
+
'properties': properties.copy()
|
127
|
+
}
|
128
|
+
|
129
|
+
# Add to build cache
|
130
|
+
self._build_cache.append((source, target, edge_data))
|
131
|
+
|
132
|
+
# For undirected graphs, add reverse edge
|
133
|
+
if not self.is_directed and source != target:
|
134
|
+
reverse_edge_data = edge_data.copy()
|
135
|
+
reverse_edge_data['source'] = target
|
136
|
+
reverse_edge_data['target'] = source
|
137
|
+
self._build_cache.append((target, source, reverse_edge_data))
|
138
|
+
|
139
|
+
self._edge_count += 1
|
140
|
+
self._needs_rebuild = True
|
141
|
+
|
142
|
+
return edge_id
|
143
|
+
|
144
|
+
def remove_edge(self, source: str, target: str, edge_id: Optional[str] = None) -> bool:
|
145
|
+
"""Remove edge between source and target."""
|
146
|
+
# For CSR, removal is expensive, so we rebuild from cache
|
147
|
+
if source not in self._vertex_to_index or target not in self._vertex_to_index:
|
148
|
+
return False
|
149
|
+
|
150
|
+
removed = False
|
151
|
+
|
152
|
+
# Remove from build cache
|
153
|
+
original_cache_size = len(self._build_cache)
|
154
|
+
if edge_id:
|
155
|
+
self._build_cache = [
|
156
|
+
(s, t, data) for s, t, data in self._build_cache
|
157
|
+
if not (s == source and t == target and data['id'] == edge_id)
|
158
|
+
]
|
159
|
+
else:
|
160
|
+
self._build_cache = [
|
161
|
+
(s, t, data) for s, t, data in self._build_cache
|
162
|
+
if not (s == source and t == target)
|
163
|
+
]
|
164
|
+
|
165
|
+
removed = len(self._build_cache) < original_cache_size
|
166
|
+
|
167
|
+
if removed:
|
168
|
+
self._edge_count -= (original_cache_size - len(self._build_cache))
|
169
|
+
self._needs_rebuild = True
|
170
|
+
|
171
|
+
return removed
|
172
|
+
|
173
|
+
def has_edge(self, source: str, target: str) -> bool:
|
174
|
+
"""Check if edge exists between source and target."""
|
175
|
+
if source not in self._vertex_to_index or target not in self._vertex_to_index:
|
176
|
+
return False
|
177
|
+
|
178
|
+
# Rebuild if needed
|
179
|
+
self._rebuild_csr()
|
180
|
+
|
181
|
+
source_idx = self._vertex_to_index[source]
|
182
|
+
target_idx = self._vertex_to_index[target]
|
183
|
+
|
184
|
+
# Binary search in the row
|
185
|
+
start = self._row_ptr[source_idx]
|
186
|
+
end = self._row_ptr[source_idx + 1]
|
187
|
+
|
188
|
+
# Use binary search to find target
|
189
|
+
pos = bisect.bisect_left(self._col_indices[start:end], target_idx)
|
190
|
+
return (pos < end - start and
|
191
|
+
self._col_indices[start + pos] == target_idx)
|
192
|
+
|
193
|
+
def get_edge_data(self, source: str, target: str) -> Optional[Dict[str, Any]]:
|
194
|
+
"""Get edge data between source and target."""
|
195
|
+
if source not in self._vertex_to_index or target not in self._vertex_to_index:
|
196
|
+
return None
|
197
|
+
|
198
|
+
# Rebuild if needed
|
199
|
+
self._rebuild_csr()
|
200
|
+
|
201
|
+
source_idx = self._vertex_to_index[source]
|
202
|
+
target_idx = self._vertex_to_index[target]
|
203
|
+
|
204
|
+
# Binary search in the row
|
205
|
+
start = self._row_ptr[source_idx]
|
206
|
+
end = self._row_ptr[source_idx + 1]
|
207
|
+
|
208
|
+
pos = bisect.bisect_left(self._col_indices[start:end], target_idx)
|
209
|
+
if pos < end - start and self._col_indices[start + pos] == target_idx:
|
210
|
+
return self._values[start + pos]
|
211
|
+
|
212
|
+
return None
|
213
|
+
|
214
|
+
def neighbors(self, vertex: str, direction: str = 'out') -> Iterator[str]:
|
215
|
+
"""Get neighbors of a vertex."""
|
216
|
+
if vertex not in self._vertex_to_index:
|
217
|
+
return
|
218
|
+
|
219
|
+
# Rebuild if needed
|
220
|
+
self._rebuild_csr()
|
221
|
+
|
222
|
+
vertex_idx = self._vertex_to_index[vertex]
|
223
|
+
|
224
|
+
if direction == 'out':
|
225
|
+
# Outgoing neighbors (direct from CSR)
|
226
|
+
start = self._row_ptr[vertex_idx]
|
227
|
+
end = self._row_ptr[vertex_idx + 1]
|
228
|
+
|
229
|
+
for i in range(start, end):
|
230
|
+
target_idx = self._col_indices[i]
|
231
|
+
yield self._index_to_vertex[target_idx]
|
232
|
+
|
233
|
+
elif direction == 'in':
|
234
|
+
# Incoming neighbors (scan all rows)
|
235
|
+
for source_idx in range(self._vertex_count):
|
236
|
+
start = self._row_ptr[source_idx]
|
237
|
+
end = self._row_ptr[source_idx + 1]
|
238
|
+
|
239
|
+
for i in range(start, end):
|
240
|
+
if self._col_indices[i] == vertex_idx:
|
241
|
+
yield self._index_to_vertex[source_idx]
|
242
|
+
break
|
243
|
+
|
244
|
+
elif direction == 'both':
|
245
|
+
# All neighbors
|
246
|
+
seen = set()
|
247
|
+
for neighbor in self.neighbors(vertex, 'out'):
|
248
|
+
if neighbor not in seen:
|
249
|
+
seen.add(neighbor)
|
250
|
+
yield neighbor
|
251
|
+
for neighbor in self.neighbors(vertex, 'in'):
|
252
|
+
if neighbor not in seen:
|
253
|
+
seen.add(neighbor)
|
254
|
+
yield neighbor
|
255
|
+
|
256
|
+
def degree(self, vertex: str, direction: str = 'out') -> int:
|
257
|
+
"""Get degree of a vertex."""
|
258
|
+
if vertex not in self._vertex_to_index:
|
259
|
+
return 0
|
260
|
+
|
261
|
+
# Rebuild if needed
|
262
|
+
self._rebuild_csr()
|
263
|
+
|
264
|
+
vertex_idx = self._vertex_to_index[vertex]
|
265
|
+
|
266
|
+
if direction == 'out':
|
267
|
+
# Out-degree from row pointer difference
|
268
|
+
return self._row_ptr[vertex_idx + 1] - self._row_ptr[vertex_idx]
|
269
|
+
|
270
|
+
elif direction == 'in':
|
271
|
+
# In-degree by scanning columns
|
272
|
+
count = 0
|
273
|
+
for i in range(len(self._col_indices)):
|
274
|
+
if self._col_indices[i] == vertex_idx:
|
275
|
+
count += 1
|
276
|
+
return count
|
277
|
+
|
278
|
+
elif direction == 'both':
|
279
|
+
out_degree = self.degree(vertex, 'out')
|
280
|
+
in_degree = self.degree(vertex, 'in')
|
281
|
+
return out_degree if not self.is_directed else out_degree + in_degree
|
282
|
+
|
283
|
+
def edges(self, data: bool = False) -> Iterator[tuple]:
|
284
|
+
"""Get all edges in the graph."""
|
285
|
+
# Rebuild if needed
|
286
|
+
self._rebuild_csr()
|
287
|
+
|
288
|
+
for source_idx in range(self._vertex_count):
|
289
|
+
start = self._row_ptr[source_idx]
|
290
|
+
end = self._row_ptr[source_idx + 1]
|
291
|
+
|
292
|
+
source = self._index_to_vertex[source_idx]
|
293
|
+
|
294
|
+
for i in range(start, end):
|
295
|
+
target_idx = self._col_indices[i]
|
296
|
+
target = self._index_to_vertex[target_idx]
|
297
|
+
|
298
|
+
# For undirected graphs, avoid returning duplicate edges
|
299
|
+
if not self.is_directed and source > target:
|
300
|
+
continue
|
301
|
+
|
302
|
+
if data:
|
303
|
+
yield (source, target, self._values[i])
|
304
|
+
else:
|
305
|
+
yield (source, target)
|
306
|
+
|
307
|
+
def vertices(self) -> Iterator[str]:
|
308
|
+
"""Get all vertices in the graph."""
|
309
|
+
return iter(self._vertex_to_index.keys())
|
310
|
+
|
311
|
+
def __len__(self) -> int:
|
312
|
+
"""Get the number of edges."""
|
313
|
+
return self._edge_count
|
314
|
+
|
315
|
+
def vertex_count(self) -> int:
|
316
|
+
"""Get the number of vertices."""
|
317
|
+
return self._vertex_count
|
318
|
+
|
319
|
+
def clear(self) -> None:
|
320
|
+
"""Clear all edges and vertices."""
|
321
|
+
self._row_ptr = [0]
|
322
|
+
self._col_indices.clear()
|
323
|
+
self._values.clear()
|
324
|
+
self._vertex_to_index.clear()
|
325
|
+
self._index_to_vertex.clear()
|
326
|
+
self._vertex_count = 0
|
327
|
+
self._edge_count = 0
|
328
|
+
self._edge_id_counter = 0
|
329
|
+
self._build_cache.clear()
|
330
|
+
self._needs_rebuild = False
|
331
|
+
|
332
|
+
def add_vertex(self, vertex: str) -> None:
|
333
|
+
"""Add a vertex to the graph."""
|
334
|
+
if vertex not in self._vertex_to_index:
|
335
|
+
self._get_vertex_index(vertex)
|
336
|
+
|
337
|
+
def remove_vertex(self, vertex: str) -> bool:
|
338
|
+
"""Remove a vertex and all its edges."""
|
339
|
+
if vertex not in self._vertex_to_index:
|
340
|
+
return False
|
341
|
+
|
342
|
+
# Remove all edges involving this vertex from cache
|
343
|
+
original_cache_size = len(self._build_cache)
|
344
|
+
self._build_cache = [
|
345
|
+
(s, t, data) for s, t, data in self._build_cache
|
346
|
+
if s != vertex and t != vertex
|
347
|
+
]
|
348
|
+
|
349
|
+
edges_removed = original_cache_size - len(self._build_cache)
|
350
|
+
self._edge_count -= edges_removed
|
351
|
+
|
352
|
+
# Remove vertex from mappings
|
353
|
+
vertex_idx = self._vertex_to_index[vertex]
|
354
|
+
del self._vertex_to_index[vertex]
|
355
|
+
del self._index_to_vertex[vertex_idx]
|
356
|
+
|
357
|
+
# Compact indices (expensive operation)
|
358
|
+
self._compact_indices()
|
359
|
+
|
360
|
+
self._needs_rebuild = True
|
361
|
+
return True
|
362
|
+
|
363
|
+
def _compact_indices(self) -> None:
|
364
|
+
"""Compact vertex indices after vertex removal."""
|
365
|
+
# Create new mapping with compacted indices
|
366
|
+
old_to_new = {}
|
367
|
+
new_vertex_to_index = {}
|
368
|
+
new_index_to_vertex = {}
|
369
|
+
|
370
|
+
new_index = 0
|
371
|
+
for old_index in sorted(self._index_to_vertex.keys()):
|
372
|
+
vertex = self._index_to_vertex[old_index]
|
373
|
+
old_to_new[old_index] = new_index
|
374
|
+
new_vertex_to_index[vertex] = new_index
|
375
|
+
new_index_to_vertex[new_index] = vertex
|
376
|
+
new_index += 1
|
377
|
+
|
378
|
+
# Update mappings
|
379
|
+
self._vertex_to_index = new_vertex_to_index
|
380
|
+
self._index_to_vertex = new_index_to_vertex
|
381
|
+
self._vertex_count = len(new_vertex_to_index)
|
382
|
+
|
383
|
+
# Update build cache with new indices
|
384
|
+
updated_cache = []
|
385
|
+
for source, target, edge_data in self._build_cache:
|
386
|
+
if source in self._vertex_to_index and target in self._vertex_to_index:
|
387
|
+
updated_cache.append((source, target, edge_data))
|
388
|
+
|
389
|
+
self._build_cache = updated_cache
|
390
|
+
|
391
|
+
# ============================================================================
|
392
|
+
# CSR-SPECIFIC OPERATIONS
|
393
|
+
# ============================================================================
|
394
|
+
|
395
|
+
def get_csr_arrays(self) -> Tuple[List[int], List[int], List[float]]:
|
396
|
+
"""Get the raw CSR arrays (row_ptr, col_indices, weights)."""
|
397
|
+
self._rebuild_csr()
|
398
|
+
|
399
|
+
weights = [edge_data.get('weight', 1.0) for edge_data in self._values]
|
400
|
+
return self._row_ptr.copy(), self._col_indices.copy(), weights
|
401
|
+
|
402
|
+
def from_csr_arrays(self, row_ptr: List[int], col_indices: List[int],
|
403
|
+
weights: List[float], vertices: List[str]) -> None:
|
404
|
+
"""Build graph from CSR arrays."""
|
405
|
+
if len(row_ptr) != len(vertices) + 1:
|
406
|
+
raise ValueError("row_ptr length must be vertices + 1")
|
407
|
+
if len(col_indices) != len(weights):
|
408
|
+
raise ValueError("col_indices and weights must have same length")
|
409
|
+
|
410
|
+
# Clear existing data
|
411
|
+
self.clear()
|
412
|
+
|
413
|
+
# Add vertices
|
414
|
+
for vertex in vertices:
|
415
|
+
self.add_vertex(vertex)
|
416
|
+
|
417
|
+
# Add edges from CSR format
|
418
|
+
for source_idx, vertex in enumerate(vertices):
|
419
|
+
start = row_ptr[source_idx]
|
420
|
+
end = row_ptr[source_idx + 1]
|
421
|
+
|
422
|
+
for i in range(start, end):
|
423
|
+
target_idx = col_indices[i]
|
424
|
+
weight = weights[i]
|
425
|
+
target = vertices[target_idx]
|
426
|
+
|
427
|
+
self.add_edge(vertex, target, weight=weight)
|
428
|
+
|
429
|
+
def multiply_vector(self, vector: List[float]) -> List[float]:
|
430
|
+
"""Multiply the adjacency matrix by a vector (SpMV operation)."""
|
431
|
+
if len(vector) != self._vertex_count:
|
432
|
+
raise ValueError(f"Vector length {len(vector)} must match vertex count {self._vertex_count}")
|
433
|
+
|
434
|
+
# Rebuild if needed
|
435
|
+
self._rebuild_csr()
|
436
|
+
|
437
|
+
result = [0.0] * self._vertex_count
|
438
|
+
|
439
|
+
for source_idx in range(self._vertex_count):
|
440
|
+
start = self._row_ptr[source_idx]
|
441
|
+
end = self._row_ptr[source_idx + 1]
|
442
|
+
|
443
|
+
for i in range(start, end):
|
444
|
+
target_idx = self._col_indices[i]
|
445
|
+
weight = self._values[i].get('weight', 1.0)
|
446
|
+
result[source_idx] += weight * vector[target_idx]
|
447
|
+
|
448
|
+
return result
|
449
|
+
|
450
|
+
def get_compression_ratio(self) -> float:
|
451
|
+
"""Get the compression ratio compared to dense matrix."""
|
452
|
+
dense_size = self._vertex_count * self._vertex_count
|
453
|
+
sparse_size = len(self._col_indices) + len(self._row_ptr) + len(self._values)
|
454
|
+
return sparse_size / max(1, dense_size)
|
455
|
+
|
456
|
+
# ============================================================================
|
457
|
+
# PERFORMANCE CHARACTERISTICS
|
458
|
+
# ============================================================================
|
459
|
+
|
460
|
+
@property
|
461
|
+
def backend_info(self) -> Dict[str, Any]:
|
462
|
+
"""Get backend implementation info."""
|
463
|
+
return {
|
464
|
+
'strategy': 'CSR',
|
465
|
+
'backend': 'Compressed Sparse Row format',
|
466
|
+
'directed': self.is_directed,
|
467
|
+
'compression': True,
|
468
|
+
'complexity': {
|
469
|
+
'add_edge': 'O(1) amortized',
|
470
|
+
'remove_edge': 'O(E) worst case',
|
471
|
+
'has_edge': 'O(log degree)',
|
472
|
+
'neighbors_out': 'O(degree)',
|
473
|
+
'neighbors_in': 'O(E)',
|
474
|
+
'space': 'O(V + E)'
|
475
|
+
}
|
476
|
+
}
|
477
|
+
|
478
|
+
@property
|
479
|
+
def metrics(self) -> Dict[str, Any]:
|
480
|
+
"""Get performance metrics."""
|
481
|
+
compression_ratio = self.get_compression_ratio()
|
482
|
+
avg_degree = self._edge_count / max(1, self._vertex_count) if self._vertex_count else 0
|
483
|
+
|
484
|
+
return {
|
485
|
+
'vertices': self._vertex_count,
|
486
|
+
'edges': self._edge_count,
|
487
|
+
'compression_ratio': round(compression_ratio, 4),
|
488
|
+
'average_degree': round(avg_degree, 2),
|
489
|
+
'memory_usage': f"{len(self._col_indices) * 12 + len(self._row_ptr) * 4} bytes (estimated)",
|
490
|
+
'cache_size': len(self._build_cache),
|
491
|
+
'needs_rebuild': self._needs_rebuild
|
492
|
+
}
|