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.
Files changed (132) hide show
  1. exonware/__init__.py +14 -0
  2. exonware/xwnode/__init__.py +127 -0
  3. exonware/xwnode/base.py +676 -0
  4. exonware/xwnode/config.py +178 -0
  5. exonware/xwnode/contracts.py +730 -0
  6. exonware/xwnode/errors.py +503 -0
  7. exonware/xwnode/facade.py +460 -0
  8. exonware/xwnode/strategies/__init__.py +158 -0
  9. exonware/xwnode/strategies/advisor.py +463 -0
  10. exonware/xwnode/strategies/edges/__init__.py +32 -0
  11. exonware/xwnode/strategies/edges/adj_list.py +227 -0
  12. exonware/xwnode/strategies/edges/adj_matrix.py +391 -0
  13. exonware/xwnode/strategies/edges/base.py +169 -0
  14. exonware/xwnode/strategies/flyweight.py +328 -0
  15. exonware/xwnode/strategies/impls/__init__.py +13 -0
  16. exonware/xwnode/strategies/impls/_base_edge.py +403 -0
  17. exonware/xwnode/strategies/impls/_base_node.py +307 -0
  18. exonware/xwnode/strategies/impls/edge_adj_list.py +353 -0
  19. exonware/xwnode/strategies/impls/edge_adj_matrix.py +445 -0
  20. exonware/xwnode/strategies/impls/edge_bidir_wrapper.py +455 -0
  21. exonware/xwnode/strategies/impls/edge_block_adj_matrix.py +539 -0
  22. exonware/xwnode/strategies/impls/edge_coo.py +533 -0
  23. exonware/xwnode/strategies/impls/edge_csc.py +447 -0
  24. exonware/xwnode/strategies/impls/edge_csr.py +492 -0
  25. exonware/xwnode/strategies/impls/edge_dynamic_adj_list.py +503 -0
  26. exonware/xwnode/strategies/impls/edge_flow_network.py +555 -0
  27. exonware/xwnode/strategies/impls/edge_hyperedge_set.py +516 -0
  28. exonware/xwnode/strategies/impls/edge_neural_graph.py +650 -0
  29. exonware/xwnode/strategies/impls/edge_octree.py +574 -0
  30. exonware/xwnode/strategies/impls/edge_property_store.py +655 -0
  31. exonware/xwnode/strategies/impls/edge_quadtree.py +519 -0
  32. exonware/xwnode/strategies/impls/edge_rtree.py +820 -0
  33. exonware/xwnode/strategies/impls/edge_temporal_edgeset.py +558 -0
  34. exonware/xwnode/strategies/impls/edge_tree_graph_basic.py +271 -0
  35. exonware/xwnode/strategies/impls/edge_weighted_graph.py +411 -0
  36. exonware/xwnode/strategies/manager.py +775 -0
  37. exonware/xwnode/strategies/metrics.py +538 -0
  38. exonware/xwnode/strategies/migration.py +432 -0
  39. exonware/xwnode/strategies/nodes/__init__.py +50 -0
  40. exonware/xwnode/strategies/nodes/_base_node.py +307 -0
  41. exonware/xwnode/strategies/nodes/adjacency_list.py +267 -0
  42. exonware/xwnode/strategies/nodes/aho_corasick.py +345 -0
  43. exonware/xwnode/strategies/nodes/array_list.py +209 -0
  44. exonware/xwnode/strategies/nodes/base.py +247 -0
  45. exonware/xwnode/strategies/nodes/deque.py +200 -0
  46. exonware/xwnode/strategies/nodes/hash_map.py +135 -0
  47. exonware/xwnode/strategies/nodes/heap.py +307 -0
  48. exonware/xwnode/strategies/nodes/linked_list.py +232 -0
  49. exonware/xwnode/strategies/nodes/node_aho_corasick.py +520 -0
  50. exonware/xwnode/strategies/nodes/node_array_list.py +175 -0
  51. exonware/xwnode/strategies/nodes/node_avl_tree.py +371 -0
  52. exonware/xwnode/strategies/nodes/node_b_plus_tree.py +542 -0
  53. exonware/xwnode/strategies/nodes/node_bitmap.py +420 -0
  54. exonware/xwnode/strategies/nodes/node_bitset_dynamic.py +513 -0
  55. exonware/xwnode/strategies/nodes/node_bloom_filter.py +347 -0
  56. exonware/xwnode/strategies/nodes/node_btree.py +357 -0
  57. exonware/xwnode/strategies/nodes/node_count_min_sketch.py +470 -0
  58. exonware/xwnode/strategies/nodes/node_cow_tree.py +473 -0
  59. exonware/xwnode/strategies/nodes/node_cuckoo_hash.py +392 -0
  60. exonware/xwnode/strategies/nodes/node_fenwick_tree.py +301 -0
  61. exonware/xwnode/strategies/nodes/node_hash_map.py +269 -0
  62. exonware/xwnode/strategies/nodes/node_heap.py +191 -0
  63. exonware/xwnode/strategies/nodes/node_hyperloglog.py +407 -0
  64. exonware/xwnode/strategies/nodes/node_linked_list.py +409 -0
  65. exonware/xwnode/strategies/nodes/node_lsm_tree.py +400 -0
  66. exonware/xwnode/strategies/nodes/node_ordered_map.py +390 -0
  67. exonware/xwnode/strategies/nodes/node_ordered_map_balanced.py +565 -0
  68. exonware/xwnode/strategies/nodes/node_patricia.py +512 -0
  69. exonware/xwnode/strategies/nodes/node_persistent_tree.py +378 -0
  70. exonware/xwnode/strategies/nodes/node_radix_trie.py +452 -0
  71. exonware/xwnode/strategies/nodes/node_red_black_tree.py +497 -0
  72. exonware/xwnode/strategies/nodes/node_roaring_bitmap.py +570 -0
  73. exonware/xwnode/strategies/nodes/node_segment_tree.py +289 -0
  74. exonware/xwnode/strategies/nodes/node_set_hash.py +354 -0
  75. exonware/xwnode/strategies/nodes/node_set_tree.py +480 -0
  76. exonware/xwnode/strategies/nodes/node_skip_list.py +316 -0
  77. exonware/xwnode/strategies/nodes/node_splay_tree.py +393 -0
  78. exonware/xwnode/strategies/nodes/node_suffix_array.py +487 -0
  79. exonware/xwnode/strategies/nodes/node_treap.py +387 -0
  80. exonware/xwnode/strategies/nodes/node_tree_graph_hybrid.py +1434 -0
  81. exonware/xwnode/strategies/nodes/node_trie.py +252 -0
  82. exonware/xwnode/strategies/nodes/node_union_find.py +187 -0
  83. exonware/xwnode/strategies/nodes/node_xdata_optimized.py +369 -0
  84. exonware/xwnode/strategies/nodes/priority_queue.py +209 -0
  85. exonware/xwnode/strategies/nodes/queue.py +161 -0
  86. exonware/xwnode/strategies/nodes/sparse_matrix.py +206 -0
  87. exonware/xwnode/strategies/nodes/stack.py +152 -0
  88. exonware/xwnode/strategies/nodes/trie.py +274 -0
  89. exonware/xwnode/strategies/nodes/union_find.py +283 -0
  90. exonware/xwnode/strategies/pattern_detector.py +603 -0
  91. exonware/xwnode/strategies/performance_monitor.py +487 -0
  92. exonware/xwnode/strategies/queries/__init__.py +24 -0
  93. exonware/xwnode/strategies/queries/base.py +236 -0
  94. exonware/xwnode/strategies/queries/cql.py +201 -0
  95. exonware/xwnode/strategies/queries/cypher.py +181 -0
  96. exonware/xwnode/strategies/queries/datalog.py +70 -0
  97. exonware/xwnode/strategies/queries/elastic_dsl.py +70 -0
  98. exonware/xwnode/strategies/queries/eql.py +70 -0
  99. exonware/xwnode/strategies/queries/flux.py +70 -0
  100. exonware/xwnode/strategies/queries/gql.py +70 -0
  101. exonware/xwnode/strategies/queries/graphql.py +240 -0
  102. exonware/xwnode/strategies/queries/gremlin.py +181 -0
  103. exonware/xwnode/strategies/queries/hiveql.py +214 -0
  104. exonware/xwnode/strategies/queries/hql.py +70 -0
  105. exonware/xwnode/strategies/queries/jmespath.py +219 -0
  106. exonware/xwnode/strategies/queries/jq.py +66 -0
  107. exonware/xwnode/strategies/queries/json_query.py +66 -0
  108. exonware/xwnode/strategies/queries/jsoniq.py +248 -0
  109. exonware/xwnode/strategies/queries/kql.py +70 -0
  110. exonware/xwnode/strategies/queries/linq.py +238 -0
  111. exonware/xwnode/strategies/queries/logql.py +70 -0
  112. exonware/xwnode/strategies/queries/mql.py +68 -0
  113. exonware/xwnode/strategies/queries/n1ql.py +210 -0
  114. exonware/xwnode/strategies/queries/partiql.py +70 -0
  115. exonware/xwnode/strategies/queries/pig.py +215 -0
  116. exonware/xwnode/strategies/queries/promql.py +70 -0
  117. exonware/xwnode/strategies/queries/sparql.py +220 -0
  118. exonware/xwnode/strategies/queries/sql.py +275 -0
  119. exonware/xwnode/strategies/queries/xml_query.py +66 -0
  120. exonware/xwnode/strategies/queries/xpath.py +223 -0
  121. exonware/xwnode/strategies/queries/xquery.py +258 -0
  122. exonware/xwnode/strategies/queries/xwnode_executor.py +332 -0
  123. exonware/xwnode/strategies/queries/xwquery_strategy.py +424 -0
  124. exonware/xwnode/strategies/registry.py +604 -0
  125. exonware/xwnode/strategies/simple.py +273 -0
  126. exonware/xwnode/strategies/utils.py +532 -0
  127. exonware/xwnode/types.py +912 -0
  128. exonware/xwnode/version.py +78 -0
  129. exonware_xwnode-0.0.1.12.dist-info/METADATA +169 -0
  130. exonware_xwnode-0.0.1.12.dist-info/RECORD +132 -0
  131. exonware_xwnode-0.0.1.12.dist-info/WHEEL +4 -0
  132. 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
+ }