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,455 @@
|
|
1
|
+
"""
|
2
|
+
Bidirectional Wrapper Edge Strategy Implementation
|
3
|
+
|
4
|
+
This module implements the BIDIR_WRAPPER strategy for efficient
|
5
|
+
undirected graph operations using dual directed edges.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from typing import Any, Iterator, List, Dict, Set, Optional, Tuple
|
9
|
+
from collections import defaultdict
|
10
|
+
from ._base_edge import aEdgeStrategy
|
11
|
+
from ...types import EdgeMode, EdgeTrait
|
12
|
+
|
13
|
+
|
14
|
+
class xBidirWrapperStrategy(aEdgeStrategy):
|
15
|
+
"""
|
16
|
+
Bidirectional Wrapper edge strategy for undirected graphs.
|
17
|
+
|
18
|
+
Efficiently represents undirected edges using pairs of directed edges
|
19
|
+
with automatic synchronization and optimized undirected operations.
|
20
|
+
"""
|
21
|
+
|
22
|
+
def __init__(self, traits: EdgeTrait = EdgeTrait.NONE, **options):
|
23
|
+
"""Initialize the Bidirectional Wrapper strategy."""
|
24
|
+
super().__init__(EdgeMode.BIDIR_WRAPPER, traits, **options)
|
25
|
+
|
26
|
+
self.auto_sync = options.get('auto_sync', True)
|
27
|
+
self.weighted = options.get('weighted', True)
|
28
|
+
self.allow_self_loops = options.get('allow_self_loops', True)
|
29
|
+
|
30
|
+
# Core storage: directed adjacency lists for both directions
|
31
|
+
self._outgoing: Dict[str, Dict[str, float]] = defaultdict(dict) # source -> {target: weight}
|
32
|
+
self._incoming: Dict[str, Dict[str, float]] = defaultdict(dict) # target -> {source: weight}
|
33
|
+
|
34
|
+
# Undirected edge tracking
|
35
|
+
self._undirected_edges: Set[Tuple[str, str]] = set() # Canonical edge pairs (min, max)
|
36
|
+
self._vertices: Set[str] = set()
|
37
|
+
|
38
|
+
# Performance tracking
|
39
|
+
self._edge_count = 0
|
40
|
+
self._sync_operations = 0
|
41
|
+
|
42
|
+
def get_supported_traits(self) -> EdgeTrait:
|
43
|
+
"""Get the traits supported by the bidirectional wrapper strategy."""
|
44
|
+
return (EdgeTrait.SPARSE | EdgeTrait.CACHE_FRIENDLY)
|
45
|
+
|
46
|
+
def _canonical_edge(self, source: str, target: str) -> Tuple[str, str]:
|
47
|
+
"""Get canonical representation of undirected edge."""
|
48
|
+
return (min(source, target), max(source, target))
|
49
|
+
|
50
|
+
def _add_directed_edge(self, source: str, target: str, weight: float) -> None:
|
51
|
+
"""Add directed edge to internal structures."""
|
52
|
+
self._outgoing[source][target] = weight
|
53
|
+
self._incoming[target][source] = weight
|
54
|
+
self._vertices.add(source)
|
55
|
+
self._vertices.add(target)
|
56
|
+
|
57
|
+
def _remove_directed_edge(self, source: str, target: str) -> bool:
|
58
|
+
"""Remove directed edge from internal structures."""
|
59
|
+
if target in self._outgoing.get(source, {}):
|
60
|
+
del self._outgoing[source][target]
|
61
|
+
del self._incoming[target][source]
|
62
|
+
return True
|
63
|
+
return False
|
64
|
+
|
65
|
+
def _sync_undirected_edge(self, source: str, target: str, weight: float) -> None:
|
66
|
+
"""Synchronize both directions of an undirected edge."""
|
67
|
+
if self.auto_sync:
|
68
|
+
self._add_directed_edge(source, target, weight)
|
69
|
+
if source != target: # Avoid double self-loops
|
70
|
+
self._add_directed_edge(target, source, weight)
|
71
|
+
self._sync_operations += 1
|
72
|
+
|
73
|
+
def _unsync_undirected_edge(self, source: str, target: str) -> bool:
|
74
|
+
"""Remove both directions of an undirected edge."""
|
75
|
+
removed = False
|
76
|
+
if self._remove_directed_edge(source, target):
|
77
|
+
removed = True
|
78
|
+
if source != target and self._remove_directed_edge(target, source):
|
79
|
+
removed = True
|
80
|
+
|
81
|
+
if removed:
|
82
|
+
self._sync_operations += 1
|
83
|
+
|
84
|
+
return removed
|
85
|
+
|
86
|
+
# ============================================================================
|
87
|
+
# CORE EDGE OPERATIONS
|
88
|
+
# ============================================================================
|
89
|
+
|
90
|
+
def add_edge(self, source: str, target: str, **properties) -> str:
|
91
|
+
"""Add undirected edge (creates two directed edges)."""
|
92
|
+
weight = properties.get('weight', 1.0) if self.weighted else 1.0
|
93
|
+
|
94
|
+
if not self.allow_self_loops and source == target:
|
95
|
+
raise ValueError("Self-loops not allowed")
|
96
|
+
|
97
|
+
canonical = self._canonical_edge(source, target)
|
98
|
+
|
99
|
+
# Check if undirected edge already exists
|
100
|
+
if canonical in self._undirected_edges:
|
101
|
+
# Update existing edge
|
102
|
+
self._sync_undirected_edge(source, target, weight)
|
103
|
+
else:
|
104
|
+
# Add new undirected edge
|
105
|
+
self._sync_undirected_edge(source, target, weight)
|
106
|
+
self._undirected_edges.add(canonical)
|
107
|
+
self._edge_count += 1
|
108
|
+
|
109
|
+
return f"{canonical[0]}<->{canonical[1]}"
|
110
|
+
|
111
|
+
def remove_edge(self, source: str, target: str, edge_id: Optional[str] = None) -> bool:
|
112
|
+
"""Remove undirected edge (removes both directed edges)."""
|
113
|
+
canonical = self._canonical_edge(source, target)
|
114
|
+
|
115
|
+
if canonical in self._undirected_edges:
|
116
|
+
# Remove undirected edge
|
117
|
+
if self._unsync_undirected_edge(source, target):
|
118
|
+
self._undirected_edges.remove(canonical)
|
119
|
+
self._edge_count -= 1
|
120
|
+
return True
|
121
|
+
|
122
|
+
return False
|
123
|
+
|
124
|
+
def has_edge(self, source: str, target: str) -> bool:
|
125
|
+
"""Check if undirected edge exists."""
|
126
|
+
canonical = self._canonical_edge(source, target)
|
127
|
+
return canonical in self._undirected_edges
|
128
|
+
|
129
|
+
def get_edge_data(self, source: str, target: str) -> Optional[Dict[str, Any]]:
|
130
|
+
"""Get edge data."""
|
131
|
+
if not self.has_edge(source, target):
|
132
|
+
return None
|
133
|
+
|
134
|
+
# Get weight from one direction (they should be synchronized)
|
135
|
+
weight = self._outgoing.get(source, {}).get(target, 1.0)
|
136
|
+
canonical = self._canonical_edge(source, target)
|
137
|
+
|
138
|
+
return {
|
139
|
+
'source': source,
|
140
|
+
'target': target,
|
141
|
+
'canonical': canonical,
|
142
|
+
'weight': weight,
|
143
|
+
'undirected': True,
|
144
|
+
'self_loop': source == target
|
145
|
+
}
|
146
|
+
|
147
|
+
def neighbors(self, vertex: str, direction: str = 'both') -> Iterator[str]:
|
148
|
+
"""Get neighbors of vertex."""
|
149
|
+
if direction in ['out', 'both']:
|
150
|
+
for neighbor in self._outgoing.get(vertex, {}):
|
151
|
+
yield neighbor
|
152
|
+
|
153
|
+
if direction in ['in', 'both'] and direction != 'out':
|
154
|
+
# For undirected graphs, incoming and outgoing are the same
|
155
|
+
# But avoid duplicates when direction is 'both'
|
156
|
+
for neighbor in self._incoming.get(vertex, {}):
|
157
|
+
if direction == 'in' or neighbor not in self._outgoing.get(vertex, {}):
|
158
|
+
yield neighbor
|
159
|
+
|
160
|
+
def degree(self, vertex: str, direction: str = 'both') -> int:
|
161
|
+
"""Get degree of vertex."""
|
162
|
+
if direction == 'out':
|
163
|
+
return len(self._outgoing.get(vertex, {}))
|
164
|
+
elif direction == 'in':
|
165
|
+
return len(self._incoming.get(vertex, {}))
|
166
|
+
else: # both - for undirected graphs, this is just the degree
|
167
|
+
# Use set to avoid counting self-loops twice
|
168
|
+
neighbors = set()
|
169
|
+
neighbors.update(self._outgoing.get(vertex, {}))
|
170
|
+
neighbors.update(self._incoming.get(vertex, {}))
|
171
|
+
return len(neighbors)
|
172
|
+
|
173
|
+
def edges(self, data: bool = False) -> Iterator[tuple]:
|
174
|
+
"""Get all undirected edges (returns each edge once)."""
|
175
|
+
for canonical in self._undirected_edges:
|
176
|
+
source, target = canonical
|
177
|
+
|
178
|
+
if data:
|
179
|
+
edge_data = self.get_edge_data(source, target)
|
180
|
+
yield (source, target, edge_data)
|
181
|
+
else:
|
182
|
+
yield (source, target)
|
183
|
+
|
184
|
+
def vertices(self) -> Iterator[str]:
|
185
|
+
"""Get all vertices."""
|
186
|
+
return iter(self._vertices)
|
187
|
+
|
188
|
+
def __len__(self) -> int:
|
189
|
+
"""Get number of undirected edges."""
|
190
|
+
return self._edge_count
|
191
|
+
|
192
|
+
def vertex_count(self) -> int:
|
193
|
+
"""Get number of vertices."""
|
194
|
+
return len(self._vertices)
|
195
|
+
|
196
|
+
def clear(self) -> None:
|
197
|
+
"""Clear all data."""
|
198
|
+
self._outgoing.clear()
|
199
|
+
self._incoming.clear()
|
200
|
+
self._undirected_edges.clear()
|
201
|
+
self._vertices.clear()
|
202
|
+
self._edge_count = 0
|
203
|
+
self._sync_operations = 0
|
204
|
+
|
205
|
+
def add_vertex(self, vertex: str) -> None:
|
206
|
+
"""Add vertex to graph."""
|
207
|
+
self._vertices.add(vertex)
|
208
|
+
|
209
|
+
def remove_vertex(self, vertex: str) -> bool:
|
210
|
+
"""Remove vertex and all its edges."""
|
211
|
+
if vertex not in self._vertices:
|
212
|
+
return False
|
213
|
+
|
214
|
+
# Remove all edges involving this vertex
|
215
|
+
edges_to_remove = []
|
216
|
+
for source, target in self.edges():
|
217
|
+
if source == vertex or target == vertex:
|
218
|
+
edges_to_remove.append((source, target))
|
219
|
+
|
220
|
+
for source, target in edges_to_remove:
|
221
|
+
self.remove_edge(source, target)
|
222
|
+
|
223
|
+
# Remove vertex
|
224
|
+
self._vertices.discard(vertex)
|
225
|
+
self._outgoing.pop(vertex, None)
|
226
|
+
self._incoming.pop(vertex, None)
|
227
|
+
|
228
|
+
return True
|
229
|
+
|
230
|
+
# ============================================================================
|
231
|
+
# UNDIRECTED GRAPH SPECIFIC OPERATIONS
|
232
|
+
# ============================================================================
|
233
|
+
|
234
|
+
def add_undirected_edge(self, vertex1: str, vertex2: str, weight: float = 1.0) -> str:
|
235
|
+
"""Add undirected edge explicitly."""
|
236
|
+
return self.add_edge(vertex1, vertex2, weight=weight)
|
237
|
+
|
238
|
+
def get_undirected_degree(self, vertex: str) -> int:
|
239
|
+
"""Get undirected degree (number of incident edges)."""
|
240
|
+
return self.degree(vertex, 'both')
|
241
|
+
|
242
|
+
def get_all_neighbors(self, vertex: str) -> Set[str]:
|
243
|
+
"""Get all neighbors in undirected graph."""
|
244
|
+
neighbors = set()
|
245
|
+
neighbors.update(self._outgoing.get(vertex, {}))
|
246
|
+
neighbors.update(self._incoming.get(vertex, {}))
|
247
|
+
return neighbors
|
248
|
+
|
249
|
+
def is_connected_to(self, vertex1: str, vertex2: str) -> bool:
|
250
|
+
"""Check if two vertices are connected."""
|
251
|
+
return self.has_edge(vertex1, vertex2)
|
252
|
+
|
253
|
+
def get_edge_weight(self, vertex1: str, vertex2: str) -> Optional[float]:
|
254
|
+
"""Get weight of undirected edge."""
|
255
|
+
if not self.has_edge(vertex1, vertex2):
|
256
|
+
return None
|
257
|
+
|
258
|
+
# Return weight from either direction (should be same)
|
259
|
+
return self._outgoing.get(vertex1, {}).get(vertex2) or \
|
260
|
+
self._outgoing.get(vertex2, {}).get(vertex1)
|
261
|
+
|
262
|
+
def set_edge_weight(self, vertex1: str, vertex2: str, weight: float) -> bool:
|
263
|
+
"""Set weight of undirected edge."""
|
264
|
+
if not self.has_edge(vertex1, vertex2):
|
265
|
+
return False
|
266
|
+
|
267
|
+
# Update both directions
|
268
|
+
self._sync_undirected_edge(vertex1, vertex2, weight)
|
269
|
+
return True
|
270
|
+
|
271
|
+
def get_connected_components(self) -> List[Set[str]]:
|
272
|
+
"""Find connected components using DFS."""
|
273
|
+
visited = set()
|
274
|
+
components = []
|
275
|
+
|
276
|
+
for vertex in self._vertices:
|
277
|
+
if vertex not in visited:
|
278
|
+
component = set()
|
279
|
+
stack = [vertex]
|
280
|
+
|
281
|
+
while stack:
|
282
|
+
current = stack.pop()
|
283
|
+
if current not in visited:
|
284
|
+
visited.add(current)
|
285
|
+
component.add(current)
|
286
|
+
|
287
|
+
# Add all unvisited neighbors
|
288
|
+
for neighbor in self.get_all_neighbors(current):
|
289
|
+
if neighbor not in visited:
|
290
|
+
stack.append(neighbor)
|
291
|
+
|
292
|
+
if component:
|
293
|
+
components.append(component)
|
294
|
+
|
295
|
+
return components
|
296
|
+
|
297
|
+
def is_connected(self) -> bool:
|
298
|
+
"""Check if graph is connected."""
|
299
|
+
components = self.get_connected_components()
|
300
|
+
return len(components) <= 1
|
301
|
+
|
302
|
+
def spanning_tree_edges(self) -> List[Tuple[str, str, float]]:
|
303
|
+
"""Get edges of a minimum spanning tree using Kruskal's algorithm."""
|
304
|
+
# Get all edges with weights
|
305
|
+
edges = []
|
306
|
+
for source, target in self.edges():
|
307
|
+
weight = self.get_edge_weight(source, target)
|
308
|
+
edges.append((weight, source, target))
|
309
|
+
|
310
|
+
# Sort by weight
|
311
|
+
edges.sort()
|
312
|
+
|
313
|
+
# Union-Find for cycle detection
|
314
|
+
parent = {}
|
315
|
+
rank = {}
|
316
|
+
|
317
|
+
def find(x):
|
318
|
+
if x not in parent:
|
319
|
+
parent[x] = x
|
320
|
+
rank[x] = 0
|
321
|
+
if parent[x] != x:
|
322
|
+
parent[x] = find(parent[x])
|
323
|
+
return parent[x]
|
324
|
+
|
325
|
+
def union(x, y):
|
326
|
+
px, py = find(x), find(y)
|
327
|
+
if px == py:
|
328
|
+
return False
|
329
|
+
if rank[px] < rank[py]:
|
330
|
+
px, py = py, px
|
331
|
+
parent[py] = px
|
332
|
+
if rank[px] == rank[py]:
|
333
|
+
rank[px] += 1
|
334
|
+
return True
|
335
|
+
|
336
|
+
# Build MST
|
337
|
+
mst_edges = []
|
338
|
+
for weight, source, target in edges:
|
339
|
+
if union(source, target):
|
340
|
+
mst_edges.append((source, target, weight))
|
341
|
+
|
342
|
+
return mst_edges
|
343
|
+
|
344
|
+
def validate_synchronization(self) -> Dict[str, Any]:
|
345
|
+
"""Validate that all undirected edges are properly synchronized."""
|
346
|
+
issues = []
|
347
|
+
|
348
|
+
for canonical in self._undirected_edges:
|
349
|
+
source, target = canonical
|
350
|
+
|
351
|
+
# Check forward direction
|
352
|
+
forward_weight = self._outgoing.get(source, {}).get(target)
|
353
|
+
if forward_weight is None:
|
354
|
+
issues.append(f"Missing forward edge: {source} -> {target}")
|
355
|
+
continue
|
356
|
+
|
357
|
+
# Check backward direction (skip for self-loops)
|
358
|
+
if source != target:
|
359
|
+
backward_weight = self._outgoing.get(target, {}).get(source)
|
360
|
+
if backward_weight is None:
|
361
|
+
issues.append(f"Missing backward edge: {target} -> {source}")
|
362
|
+
elif abs(forward_weight - backward_weight) > 1e-9:
|
363
|
+
issues.append(f"Weight mismatch: {source}<->{target} ({forward_weight} != {backward_weight})")
|
364
|
+
|
365
|
+
return {
|
366
|
+
'synchronized': len(issues) == 0,
|
367
|
+
'issues': issues,
|
368
|
+
'sync_operations': self._sync_operations,
|
369
|
+
'undirected_edges': len(self._undirected_edges),
|
370
|
+
'directed_edges': sum(len(adj) for adj in self._outgoing.values())
|
371
|
+
}
|
372
|
+
|
373
|
+
def get_statistics(self) -> Dict[str, Any]:
|
374
|
+
"""Get comprehensive bidirectional wrapper statistics."""
|
375
|
+
sync_status = self.validate_synchronization()
|
376
|
+
components = self.get_connected_components()
|
377
|
+
|
378
|
+
# Calculate clustering coefficient
|
379
|
+
total_clustering = 0
|
380
|
+
vertices_with_neighbors = 0
|
381
|
+
|
382
|
+
for vertex in self._vertices:
|
383
|
+
neighbors = self.get_all_neighbors(vertex)
|
384
|
+
degree = len(neighbors)
|
385
|
+
|
386
|
+
if degree >= 2:
|
387
|
+
# Count triangles
|
388
|
+
triangles = 0
|
389
|
+
for n1 in neighbors:
|
390
|
+
for n2 in neighbors:
|
391
|
+
if n1 < n2 and self.has_edge(n1, n2):
|
392
|
+
triangles += 1
|
393
|
+
|
394
|
+
# Clustering coefficient for this vertex
|
395
|
+
possible_edges = degree * (degree - 1) / 2
|
396
|
+
clustering = triangles / possible_edges if possible_edges > 0 else 0
|
397
|
+
total_clustering += clustering
|
398
|
+
vertices_with_neighbors += 1
|
399
|
+
|
400
|
+
avg_clustering = total_clustering / vertices_with_neighbors if vertices_with_neighbors > 0 else 0
|
401
|
+
|
402
|
+
return {
|
403
|
+
'vertices': len(self._vertices),
|
404
|
+
'undirected_edges': self._edge_count,
|
405
|
+
'directed_edges_stored': sum(len(adj) for adj in self._outgoing.values()),
|
406
|
+
'connected_components': len(components),
|
407
|
+
'largest_component': max(len(comp) for comp in components) if components else 0,
|
408
|
+
'is_connected': len(components) <= 1,
|
409
|
+
'avg_degree': (2 * self._edge_count) / max(1, len(self._vertices)),
|
410
|
+
'avg_clustering_coefficient': avg_clustering,
|
411
|
+
'sync_status': sync_status,
|
412
|
+
'weighted': self.weighted,
|
413
|
+
'allow_self_loops': self.allow_self_loops,
|
414
|
+
'auto_sync': self.auto_sync
|
415
|
+
}
|
416
|
+
|
417
|
+
# ============================================================================
|
418
|
+
# PERFORMANCE CHARACTERISTICS
|
419
|
+
# ============================================================================
|
420
|
+
|
421
|
+
@property
|
422
|
+
def backend_info(self) -> Dict[str, Any]:
|
423
|
+
"""Get backend implementation info."""
|
424
|
+
return {
|
425
|
+
'strategy': 'BIDIR_WRAPPER',
|
426
|
+
'backend': 'Dual directed adjacency lists for undirected graphs',
|
427
|
+
'auto_sync': self.auto_sync,
|
428
|
+
'weighted': self.weighted,
|
429
|
+
'allow_self_loops': self.allow_self_loops,
|
430
|
+
'complexity': {
|
431
|
+
'add_edge': 'O(1)',
|
432
|
+
'remove_edge': 'O(1)',
|
433
|
+
'has_edge': 'O(1)',
|
434
|
+
'neighbors': 'O(degree)',
|
435
|
+
'connected_components': 'O(V + E)',
|
436
|
+
'space': 'O(2E + V)' # Double storage for undirected edges
|
437
|
+
}
|
438
|
+
}
|
439
|
+
|
440
|
+
@property
|
441
|
+
def metrics(self) -> Dict[str, Any]:
|
442
|
+
"""Get performance metrics."""
|
443
|
+
stats = self.get_statistics()
|
444
|
+
sync_status = stats['sync_status']
|
445
|
+
|
446
|
+
return {
|
447
|
+
'vertices': stats['vertices'],
|
448
|
+
'undirected_edges': stats['undirected_edges'],
|
449
|
+
'directed_edges_stored': stats['directed_edges_stored'],
|
450
|
+
'connected_components': stats['connected_components'],
|
451
|
+
'avg_degree': f"{stats['avg_degree']:.1f}",
|
452
|
+
'clustering_coeff': f"{stats['avg_clustering_coefficient']:.3f}",
|
453
|
+
'synchronized': sync_status['synchronized'],
|
454
|
+
'memory_usage': f"{stats['directed_edges_stored'] * 16 + len(self._vertices) * 50} bytes (estimated)"
|
455
|
+
}
|