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,271 @@
|
|
1
|
+
#exonware\xnode\strategies\impls\edge_tree_graph_basic.py
|
2
|
+
"""
|
3
|
+
Tree-Graph Basic Edge Strategy Implementation
|
4
|
+
|
5
|
+
This module implements the TREE_GRAPH_BASIC strategy for basic edge storage
|
6
|
+
in tree+graph hybrid structures, providing minimal graph capabilities.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from typing import Any, Dict, List, Optional, Set, Tuple, Iterator
|
10
|
+
from ._base_edge import aEdgeStrategy
|
11
|
+
from ...types import EdgeMode, EdgeTrait
|
12
|
+
|
13
|
+
|
14
|
+
class xTreeGraphBasicStrategy(aEdgeStrategy):
|
15
|
+
"""
|
16
|
+
Basic edge strategy for tree+graph hybrid structures.
|
17
|
+
|
18
|
+
Provides minimal graph capabilities optimized for tree navigation
|
19
|
+
with basic edge storage and traversal support.
|
20
|
+
"""
|
21
|
+
|
22
|
+
def __init__(self, traits: EdgeTrait = EdgeTrait.NONE, **options):
|
23
|
+
"""Initialize the tree-graph basic strategy."""
|
24
|
+
super().__init__(EdgeMode.TREE_GRAPH_BASIC, traits, **options)
|
25
|
+
|
26
|
+
# Basic edge storage - simple adjacency representation
|
27
|
+
self._edges: Dict[str, Set[str]] = {} # source -> {targets}
|
28
|
+
self._reverse_edges: Dict[str, Set[str]] = {} # target -> {sources}
|
29
|
+
self._edge_count = 0
|
30
|
+
|
31
|
+
# Statistics
|
32
|
+
self._total_additions = 0
|
33
|
+
self._total_removals = 0
|
34
|
+
self._max_degree = 0
|
35
|
+
|
36
|
+
def get_supported_traits(self) -> EdgeTrait:
|
37
|
+
"""Get the traits supported by the tree-graph basic strategy."""
|
38
|
+
return (EdgeTrait.DIRECTED | EdgeTrait.WEIGHTED | EdgeTrait.MULTI)
|
39
|
+
|
40
|
+
def _update_degree_stats(self, node: str) -> None:
|
41
|
+
"""Update degree statistics."""
|
42
|
+
degree = len(self._edges.get(node, set()))
|
43
|
+
self._max_degree = max(self._max_degree, degree)
|
44
|
+
|
45
|
+
def _add_edge_internal(self, source: str, target: str, weight: float = 1.0,
|
46
|
+
metadata: Optional[Dict[str, Any]] = None) -> bool:
|
47
|
+
"""Internal method to add edge."""
|
48
|
+
if source not in self._edges:
|
49
|
+
self._edges[source] = set()
|
50
|
+
if target not in self._reverse_edges:
|
51
|
+
self._reverse_edges[target] = set()
|
52
|
+
|
53
|
+
# Check if edge already exists
|
54
|
+
if target in self._edges[source]:
|
55
|
+
return False # Edge already exists
|
56
|
+
|
57
|
+
# Add edge
|
58
|
+
self._edges[source].add(target)
|
59
|
+
self._reverse_edges[target].add(source)
|
60
|
+
self._edge_count += 1
|
61
|
+
self._total_additions += 1
|
62
|
+
|
63
|
+
# Update statistics
|
64
|
+
self._update_degree_stats(source)
|
65
|
+
|
66
|
+
return True
|
67
|
+
|
68
|
+
def _remove_edge_internal(self, source: str, target: str) -> bool:
|
69
|
+
"""Internal method to remove edge."""
|
70
|
+
if source not in self._edges or target not in self._edges[source]:
|
71
|
+
return False
|
72
|
+
|
73
|
+
# Remove edge
|
74
|
+
self._edges[source].remove(target)
|
75
|
+
self._reverse_edges[target].remove(source)
|
76
|
+
self._edge_count -= 1
|
77
|
+
self._total_removals += 1
|
78
|
+
|
79
|
+
# Clean up empty sets
|
80
|
+
if not self._edges[source]:
|
81
|
+
del self._edges[source]
|
82
|
+
if not self._reverse_edges[target]:
|
83
|
+
del self._reverse_edges[target]
|
84
|
+
|
85
|
+
return True
|
86
|
+
|
87
|
+
# ============================================================================
|
88
|
+
# CORE OPERATIONS
|
89
|
+
# ============================================================================
|
90
|
+
|
91
|
+
def add_edge(self, source: str, target: str, weight: float = 1.0,
|
92
|
+
metadata: Optional[Dict[str, Any]] = None) -> bool:
|
93
|
+
"""Add an edge between source and target nodes."""
|
94
|
+
if not isinstance(source, str) or not isinstance(target, str):
|
95
|
+
raise ValueError("Source and target must be strings")
|
96
|
+
|
97
|
+
return self._add_edge_internal(source, target, weight, metadata)
|
98
|
+
|
99
|
+
def remove_edge(self, source: str, target: str) -> bool:
|
100
|
+
"""Remove an edge between source and target nodes."""
|
101
|
+
if not isinstance(source, str) or not isinstance(target, str):
|
102
|
+
raise ValueError("Source and target must be strings")
|
103
|
+
|
104
|
+
return self._remove_edge_internal(source, target)
|
105
|
+
|
106
|
+
def has_edge(self, source: str, target: str) -> bool:
|
107
|
+
"""Check if an edge exists between source and target nodes."""
|
108
|
+
if not isinstance(source, str) or not isinstance(target, str):
|
109
|
+
return False
|
110
|
+
|
111
|
+
return source in self._edges and target in self._edges[source]
|
112
|
+
|
113
|
+
def get_neighbors(self, node: str) -> List[str]:
|
114
|
+
"""Get all neighbors of a node."""
|
115
|
+
if not isinstance(node, str):
|
116
|
+
return []
|
117
|
+
|
118
|
+
return list(self._edges.get(node, set()))
|
119
|
+
|
120
|
+
def get_incoming(self, node: str) -> List[str]:
|
121
|
+
"""Get all incoming neighbors of a node."""
|
122
|
+
if not isinstance(node, str):
|
123
|
+
return []
|
124
|
+
|
125
|
+
return list(self._reverse_edges.get(node, set()))
|
126
|
+
|
127
|
+
def get_outgoing(self, node: str) -> List[str]:
|
128
|
+
"""Get all outgoing neighbors of a node."""
|
129
|
+
if not isinstance(node, str):
|
130
|
+
return []
|
131
|
+
|
132
|
+
return list(self._edges.get(node, set()))
|
133
|
+
|
134
|
+
def get_degree(self, node: str) -> int:
|
135
|
+
"""Get the degree (number of neighbors) of a node."""
|
136
|
+
if not isinstance(node, str):
|
137
|
+
return 0
|
138
|
+
|
139
|
+
return len(self._edges.get(node, set()))
|
140
|
+
|
141
|
+
def get_in_degree(self, node: str) -> int:
|
142
|
+
"""Get the in-degree of a node."""
|
143
|
+
if not isinstance(node, str):
|
144
|
+
return 0
|
145
|
+
|
146
|
+
return len(self._reverse_edges.get(node, set()))
|
147
|
+
|
148
|
+
def get_out_degree(self, node: str) -> int:
|
149
|
+
"""Get the out-degree of a node."""
|
150
|
+
if not isinstance(node, str):
|
151
|
+
return 0
|
152
|
+
|
153
|
+
return len(self._edges.get(node, set()))
|
154
|
+
|
155
|
+
def clear(self) -> None:
|
156
|
+
"""Clear all edges."""
|
157
|
+
self._edges.clear()
|
158
|
+
self._reverse_edges.clear()
|
159
|
+
self._edge_count = 0
|
160
|
+
|
161
|
+
def size(self) -> int:
|
162
|
+
"""Get the number of edges."""
|
163
|
+
return self._edge_count
|
164
|
+
|
165
|
+
def is_empty(self) -> bool:
|
166
|
+
"""Check if there are no edges."""
|
167
|
+
return self._edge_count == 0
|
168
|
+
|
169
|
+
def get_nodes(self) -> Set[str]:
|
170
|
+
"""Get all nodes that have edges."""
|
171
|
+
nodes = set()
|
172
|
+
nodes.update(self._edges.keys())
|
173
|
+
nodes.update(self._reverse_edges.keys())
|
174
|
+
return nodes
|
175
|
+
|
176
|
+
def get_edge_count(self) -> int:
|
177
|
+
"""Get the total number of edges."""
|
178
|
+
return self._edge_count
|
179
|
+
|
180
|
+
# ============================================================================
|
181
|
+
# ITERATION
|
182
|
+
# ============================================================================
|
183
|
+
|
184
|
+
def edges(self) -> Iterator[Tuple[str, str]]:
|
185
|
+
"""Iterate over all edges as (source, target) pairs."""
|
186
|
+
for source, targets in self._edges.items():
|
187
|
+
for target in targets:
|
188
|
+
yield (source, target)
|
189
|
+
|
190
|
+
def nodes(self) -> Iterator[str]:
|
191
|
+
"""Iterate over all nodes."""
|
192
|
+
yield from self.get_nodes()
|
193
|
+
|
194
|
+
def __iter__(self) -> Iterator[Tuple[str, str]]:
|
195
|
+
"""Iterate over all edges."""
|
196
|
+
yield from self.edges()
|
197
|
+
|
198
|
+
# ============================================================================
|
199
|
+
# TREE-GRAPH BASIC SPECIFIC OPERATIONS
|
200
|
+
# ============================================================================
|
201
|
+
|
202
|
+
def get_children(self, node: str) -> List[str]:
|
203
|
+
"""Get children of a node (for tree-like navigation)."""
|
204
|
+
return self.get_outgoing(node)
|
205
|
+
|
206
|
+
def get_parents(self, node: str) -> List[str]:
|
207
|
+
"""Get parents of a node (for tree-like navigation)."""
|
208
|
+
return self.get_incoming(node)
|
209
|
+
|
210
|
+
def is_leaf(self, node: str) -> bool:
|
211
|
+
"""Check if a node is a leaf (no outgoing edges)."""
|
212
|
+
return self.get_out_degree(node) == 0
|
213
|
+
|
214
|
+
def is_root(self, node: str) -> bool:
|
215
|
+
"""Check if a node is a root (no incoming edges)."""
|
216
|
+
return self.get_in_degree(node) == 0
|
217
|
+
|
218
|
+
def get_roots(self) -> List[str]:
|
219
|
+
"""Get all root nodes (nodes with no incoming edges)."""
|
220
|
+
all_nodes = self.get_nodes()
|
221
|
+
return [node for node in all_nodes if self.is_root(node)]
|
222
|
+
|
223
|
+
def get_leaves(self) -> List[str]:
|
224
|
+
"""Get all leaf nodes (nodes with no outgoing edges)."""
|
225
|
+
all_nodes = self.get_nodes()
|
226
|
+
return [node for node in all_nodes if self.is_leaf(node)]
|
227
|
+
|
228
|
+
def get_path(self, source: str, target: str) -> Optional[List[str]]:
|
229
|
+
"""Get a simple path from source to target using BFS."""
|
230
|
+
if source == target:
|
231
|
+
return [source]
|
232
|
+
|
233
|
+
if source not in self._edges:
|
234
|
+
return None
|
235
|
+
|
236
|
+
# Simple BFS for path finding
|
237
|
+
queue = [(source, [source])]
|
238
|
+
visited = {source}
|
239
|
+
|
240
|
+
while queue:
|
241
|
+
current, path = queue.pop(0)
|
242
|
+
|
243
|
+
for neighbor in self._edges.get(current, set()):
|
244
|
+
if neighbor == target:
|
245
|
+
return path + [neighbor]
|
246
|
+
|
247
|
+
if neighbor not in visited:
|
248
|
+
visited.add(neighbor)
|
249
|
+
queue.append((neighbor, path + [neighbor]))
|
250
|
+
|
251
|
+
return None
|
252
|
+
|
253
|
+
def is_connected(self, source: str, target: str) -> bool:
|
254
|
+
"""Check if two nodes are connected."""
|
255
|
+
return self.get_path(source, target) is not None
|
256
|
+
|
257
|
+
def get_stats(self) -> Dict[str, Any]:
|
258
|
+
"""Get performance statistics."""
|
259
|
+
nodes = self.get_nodes()
|
260
|
+
return {
|
261
|
+
'edge_count': self._edge_count,
|
262
|
+
'node_count': len(nodes),
|
263
|
+
'max_degree': self._max_degree,
|
264
|
+
'total_additions': self._total_additions,
|
265
|
+
'total_removals': self._total_removals,
|
266
|
+
'roots': len(self.get_roots()),
|
267
|
+
'leaves': len(self.get_leaves()),
|
268
|
+
'strategy': 'TREE_GRAPH_BASIC',
|
269
|
+
'backend': 'Simple adjacency sets with reverse indexing',
|
270
|
+
'traits': [trait.name for trait in EdgeTrait if self.has_trait(trait)]
|
271
|
+
}
|
@@ -0,0 +1,411 @@
|
|
1
|
+
#exonware\xwnode\strategies\impls\edge_weighted_graph.py
|
2
|
+
"""
|
3
|
+
Weighted Graph Edge Strategy Implementation
|
4
|
+
|
5
|
+
This module implements the WEIGHTED_GRAPH strategy for graphs with numerical
|
6
|
+
edge weights, optimized for network algorithms and shortest path computations.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from typing import Any, Dict, List, Optional, Set, Tuple, Iterator
|
10
|
+
from ._base_edge import aEdgeStrategy
|
11
|
+
from ...types import EdgeMode, EdgeTrait
|
12
|
+
from ...errors import XWNodeUnsupportedCapabilityError
|
13
|
+
import threading
|
14
|
+
|
15
|
+
|
16
|
+
class WeightedEdge:
|
17
|
+
"""Weighted edge with source, target, and weight."""
|
18
|
+
|
19
|
+
def __init__(self, source: str, target: str, weight: float = 1.0, data: Any = None):
|
20
|
+
self.source = source
|
21
|
+
self.target = target
|
22
|
+
self.weight = weight
|
23
|
+
self.data = data
|
24
|
+
self._hash = None
|
25
|
+
|
26
|
+
def __hash__(self) -> int:
|
27
|
+
"""Cache hash for performance."""
|
28
|
+
if self._hash is None:
|
29
|
+
self._hash = hash((self.source, self.target, self.weight))
|
30
|
+
return self._hash
|
31
|
+
|
32
|
+
def __eq__(self, other) -> bool:
|
33
|
+
"""Structural equality."""
|
34
|
+
if not isinstance(other, WeightedEdge):
|
35
|
+
return False
|
36
|
+
return (self.source == other.source and
|
37
|
+
self.target == other.target and
|
38
|
+
self.weight == other.weight)
|
39
|
+
|
40
|
+
def __repr__(self) -> str:
|
41
|
+
"""String representation."""
|
42
|
+
return f"WeightedEdge({self.source} -> {self.target}, weight={self.weight})"
|
43
|
+
|
44
|
+
|
45
|
+
class xWeightedGraphStrategy(aEdgeStrategy):
|
46
|
+
"""
|
47
|
+
Weighted graph edge strategy for graphs with numerical edge weights.
|
48
|
+
|
49
|
+
Provides efficient storage and retrieval of weighted edges with support
|
50
|
+
for network algorithms and shortest path computations.
|
51
|
+
"""
|
52
|
+
|
53
|
+
def __init__(self, traits: EdgeTrait = EdgeTrait.NONE, **options):
|
54
|
+
"""Initialize the weighted graph strategy."""
|
55
|
+
super().__init__(EdgeMode.WEIGHTED_GRAPH, traits, **options)
|
56
|
+
|
57
|
+
self.directed = options.get('directed', True)
|
58
|
+
self.default_weight = options.get('default_weight', 1.0)
|
59
|
+
self.weight_precision = options.get('weight_precision', 6)
|
60
|
+
|
61
|
+
# Core weighted graph storage
|
62
|
+
self._edges: Dict[Tuple[str, str], WeightedEdge] = {}
|
63
|
+
self._adjacency: Dict[str, Dict[str, float]] = {} # source -> {target: weight}
|
64
|
+
self._reverse_adjacency: Dict[str, Dict[str, float]] = {} # target -> {source: weight}
|
65
|
+
self._edge_count = 0
|
66
|
+
|
67
|
+
# Statistics
|
68
|
+
self._total_edges_added = 0
|
69
|
+
self._total_edges_removed = 0
|
70
|
+
self._total_weight_updates = 0
|
71
|
+
self._max_weight = 0.0
|
72
|
+
self._min_weight = float('inf')
|
73
|
+
|
74
|
+
# Thread safety
|
75
|
+
self._lock = threading.RLock()
|
76
|
+
|
77
|
+
def get_supported_traits(self) -> EdgeTrait:
|
78
|
+
"""Get the traits supported by the weighted graph strategy."""
|
79
|
+
return (EdgeTrait.DIRECTED | EdgeTrait.WEIGHTED | EdgeTrait.SPARSE)
|
80
|
+
|
81
|
+
def _normalize_weight(self, weight: float) -> float:
|
82
|
+
"""Normalize weight to specified precision."""
|
83
|
+
return round(weight, self.weight_precision)
|
84
|
+
|
85
|
+
def _update_weight_stats(self, weight: float) -> None:
|
86
|
+
"""Update weight statistics."""
|
87
|
+
self._max_weight = max(self._max_weight, weight)
|
88
|
+
self._min_weight = min(self._min_weight, weight)
|
89
|
+
|
90
|
+
def _add_to_adjacency(self, source: str, target: str, weight: float) -> None:
|
91
|
+
"""Add edge to adjacency structure."""
|
92
|
+
if source not in self._adjacency:
|
93
|
+
self._adjacency[source] = {}
|
94
|
+
self._adjacency[source][target] = weight
|
95
|
+
|
96
|
+
if target not in self._reverse_adjacency:
|
97
|
+
self._reverse_adjacency[target] = {}
|
98
|
+
self._reverse_adjacency[target][source] = weight
|
99
|
+
|
100
|
+
def _remove_from_adjacency(self, source: str, target: str) -> None:
|
101
|
+
"""Remove edge from adjacency structure."""
|
102
|
+
if source in self._adjacency and target in self._adjacency[source]:
|
103
|
+
del self._adjacency[source][target]
|
104
|
+
if not self._adjacency[source]:
|
105
|
+
del self._adjacency[source]
|
106
|
+
|
107
|
+
if target in self._reverse_adjacency and source in self._reverse_adjacency[target]:
|
108
|
+
del self._reverse_adjacency[target][source]
|
109
|
+
if not self._reverse_adjacency[target]:
|
110
|
+
del self._reverse_adjacency[target]
|
111
|
+
|
112
|
+
# ============================================================================
|
113
|
+
# CORE OPERATIONS
|
114
|
+
# ============================================================================
|
115
|
+
|
116
|
+
def add_edge(self, source: str, target: str, weight: float = None, data: Any = None) -> bool:
|
117
|
+
"""Add a weighted edge between source and target."""
|
118
|
+
if not isinstance(source, str) or not isinstance(target, str):
|
119
|
+
return False
|
120
|
+
|
121
|
+
if weight is None:
|
122
|
+
weight = self.default_weight
|
123
|
+
|
124
|
+
weight = self._normalize_weight(weight)
|
125
|
+
|
126
|
+
with self._lock:
|
127
|
+
edge_key = (source, target)
|
128
|
+
|
129
|
+
# Check if edge already exists
|
130
|
+
if edge_key in self._edges:
|
131
|
+
# Update existing edge
|
132
|
+
old_edge = self._edges[edge_key]
|
133
|
+
old_edge.weight = weight
|
134
|
+
old_edge.data = data
|
135
|
+
self._total_weight_updates += 1
|
136
|
+
self._update_weight_stats(weight)
|
137
|
+
return False
|
138
|
+
|
139
|
+
# Create new edge
|
140
|
+
edge = WeightedEdge(source, target, weight, data)
|
141
|
+
self._edges[edge_key] = edge
|
142
|
+
self._add_to_adjacency(source, target, weight)
|
143
|
+
|
144
|
+
# Add reverse edge if undirected
|
145
|
+
if not self.directed:
|
146
|
+
reverse_key = (target, source)
|
147
|
+
if reverse_key not in self._edges:
|
148
|
+
reverse_edge = WeightedEdge(target, source, weight, data)
|
149
|
+
self._edges[reverse_key] = reverse_edge
|
150
|
+
self._add_to_adjacency(target, source, weight)
|
151
|
+
|
152
|
+
self._edge_count += 1
|
153
|
+
self._total_edges_added += 1
|
154
|
+
self._update_weight_stats(weight)
|
155
|
+
return True
|
156
|
+
|
157
|
+
def get_edge(self, source: str, target: str) -> Optional[WeightedEdge]:
|
158
|
+
"""Get edge between source and target."""
|
159
|
+
if not isinstance(source, str) or not isinstance(target, str):
|
160
|
+
return None
|
161
|
+
|
162
|
+
with self._lock:
|
163
|
+
edge_key = (source, target)
|
164
|
+
return self._edges.get(edge_key)
|
165
|
+
|
166
|
+
def get_edge_weight(self, source: str, target: str) -> Optional[float]:
|
167
|
+
"""Get weight of edge between source and target."""
|
168
|
+
edge = self.get_edge(source, target)
|
169
|
+
return edge.weight if edge else None
|
170
|
+
|
171
|
+
def set_edge_weight(self, source: str, target: str, weight: float) -> bool:
|
172
|
+
"""Set weight of edge between source and target."""
|
173
|
+
if not isinstance(source, str) or not isinstance(target, str):
|
174
|
+
return False
|
175
|
+
|
176
|
+
weight = self._normalize_weight(weight)
|
177
|
+
|
178
|
+
with self._lock:
|
179
|
+
edge_key = (source, target)
|
180
|
+
if edge_key in self._edges:
|
181
|
+
self._edges[edge_key].weight = weight
|
182
|
+
self._adjacency[source][target] = weight
|
183
|
+
self._reverse_adjacency[target][source] = weight
|
184
|
+
|
185
|
+
# Update reverse edge if undirected
|
186
|
+
if not self.directed:
|
187
|
+
reverse_key = (target, source)
|
188
|
+
if reverse_key in self._edges:
|
189
|
+
self._edges[reverse_key].weight = weight
|
190
|
+
self._adjacency[target][source] = weight
|
191
|
+
self._reverse_adjacency[source][target] = weight
|
192
|
+
|
193
|
+
self._total_weight_updates += 1
|
194
|
+
self._update_weight_stats(weight)
|
195
|
+
return True
|
196
|
+
|
197
|
+
return False
|
198
|
+
|
199
|
+
def delete_edge(self, source: str, target: str) -> bool:
|
200
|
+
"""Remove edge between source and target."""
|
201
|
+
if not isinstance(source, str) or not isinstance(target, str):
|
202
|
+
return False
|
203
|
+
|
204
|
+
with self._lock:
|
205
|
+
edge_key = (source, target)
|
206
|
+
|
207
|
+
if edge_key in self._edges:
|
208
|
+
del self._edges[edge_key]
|
209
|
+
self._remove_from_adjacency(source, target)
|
210
|
+
|
211
|
+
# Remove reverse edge if undirected
|
212
|
+
if not self.directed:
|
213
|
+
reverse_key = (target, source)
|
214
|
+
if reverse_key in self._edges:
|
215
|
+
del self._edges[reverse_key]
|
216
|
+
self._remove_from_adjacency(target, source)
|
217
|
+
|
218
|
+
self._edge_count -= 1
|
219
|
+
self._total_edges_removed += 1
|
220
|
+
return True
|
221
|
+
|
222
|
+
return False
|
223
|
+
|
224
|
+
def has_edge(self, source: str, target: str) -> bool:
|
225
|
+
"""Check if edge exists between source and target."""
|
226
|
+
if not isinstance(source, str) or not isinstance(target, str):
|
227
|
+
return False
|
228
|
+
|
229
|
+
with self._lock:
|
230
|
+
edge_key = (source, target)
|
231
|
+
return edge_key in self._edges
|
232
|
+
|
233
|
+
def get_edges_from(self, source: str) -> Iterator[WeightedEdge]:
|
234
|
+
"""Get all edges from source node."""
|
235
|
+
if not isinstance(source, str):
|
236
|
+
return
|
237
|
+
|
238
|
+
with self._lock:
|
239
|
+
if source in self._adjacency:
|
240
|
+
for target, weight in self._adjacency[source].items():
|
241
|
+
edge_key = (source, target)
|
242
|
+
if edge_key in self._edges:
|
243
|
+
yield self._edges[edge_key]
|
244
|
+
|
245
|
+
def get_edges_to(self, target: str) -> Iterator[WeightedEdge]:
|
246
|
+
"""Get all edges to target node."""
|
247
|
+
if not isinstance(target, str):
|
248
|
+
return
|
249
|
+
|
250
|
+
with self._lock:
|
251
|
+
if target in self._reverse_adjacency:
|
252
|
+
for source, weight in self._reverse_adjacency[target].items():
|
253
|
+
edge_key = (source, target)
|
254
|
+
if edge_key in self._edges:
|
255
|
+
yield self._edges[edge_key]
|
256
|
+
|
257
|
+
def get_neighbors(self, node: str) -> Iterator[str]:
|
258
|
+
"""Get all neighbors of node."""
|
259
|
+
if not isinstance(node, str):
|
260
|
+
return
|
261
|
+
|
262
|
+
with self._lock:
|
263
|
+
if node in self._adjacency:
|
264
|
+
yield from self._adjacency[node].keys()
|
265
|
+
|
266
|
+
def get_incoming_neighbors(self, node: str) -> Iterator[str]:
|
267
|
+
"""Get all incoming neighbors of node."""
|
268
|
+
if not isinstance(node, str):
|
269
|
+
return
|
270
|
+
|
271
|
+
with self._lock:
|
272
|
+
if node in self._reverse_adjacency:
|
273
|
+
yield from self._reverse_adjacency[node].keys()
|
274
|
+
|
275
|
+
def get_outgoing_neighbors(self, node: str) -> Iterator[str]:
|
276
|
+
"""Get all outgoing neighbors of node."""
|
277
|
+
return self.get_neighbors(node)
|
278
|
+
|
279
|
+
def get_edge_count(self) -> int:
|
280
|
+
"""Get total number of edges."""
|
281
|
+
return self._edge_count
|
282
|
+
|
283
|
+
def get_node_count(self) -> int:
|
284
|
+
"""Get total number of nodes."""
|
285
|
+
with self._lock:
|
286
|
+
nodes = set()
|
287
|
+
for source, target in self._edges.keys():
|
288
|
+
nodes.add(source)
|
289
|
+
nodes.add(target)
|
290
|
+
return len(nodes)
|
291
|
+
|
292
|
+
def clear(self) -> None:
|
293
|
+
"""Clear all edges."""
|
294
|
+
with self._lock:
|
295
|
+
self._edges.clear()
|
296
|
+
self._adjacency.clear()
|
297
|
+
self._reverse_adjacency.clear()
|
298
|
+
self._edge_count = 0
|
299
|
+
|
300
|
+
# ============================================================================
|
301
|
+
# WEIGHTED GRAPH SPECIFIC OPERATIONS
|
302
|
+
# ============================================================================
|
303
|
+
|
304
|
+
def get_min_weight_edge(self) -> Optional[WeightedEdge]:
|
305
|
+
"""Get edge with minimum weight."""
|
306
|
+
if not self._edges:
|
307
|
+
return None
|
308
|
+
|
309
|
+
with self._lock:
|
310
|
+
min_edge = min(self._edges.values(), key=lambda e: e.weight)
|
311
|
+
return min_edge
|
312
|
+
|
313
|
+
def get_max_weight_edge(self) -> Optional[WeightedEdge]:
|
314
|
+
"""Get edge with maximum weight."""
|
315
|
+
if not self._edges:
|
316
|
+
return None
|
317
|
+
|
318
|
+
with self._lock:
|
319
|
+
max_edge = max(self._edges.values(), key=lambda e: e.weight)
|
320
|
+
return max_edge
|
321
|
+
|
322
|
+
def get_edges_by_weight_range(self, min_weight: float, max_weight: float) -> Iterator[WeightedEdge]:
|
323
|
+
"""Get all edges within weight range."""
|
324
|
+
with self._lock:
|
325
|
+
for edge in self._edges.values():
|
326
|
+
if min_weight <= edge.weight <= max_weight:
|
327
|
+
yield edge
|
328
|
+
|
329
|
+
def get_total_weight(self) -> float:
|
330
|
+
"""Get total weight of all edges."""
|
331
|
+
with self._lock:
|
332
|
+
return sum(edge.weight for edge in self._edges.values())
|
333
|
+
|
334
|
+
def get_average_weight(self) -> float:
|
335
|
+
"""Get average weight of all edges."""
|
336
|
+
if not self._edges:
|
337
|
+
return 0.0
|
338
|
+
|
339
|
+
return self.get_total_weight() / len(self._edges)
|
340
|
+
|
341
|
+
def get_weight_distribution(self) -> Dict[str, int]:
|
342
|
+
"""Get distribution of edge weights."""
|
343
|
+
with self._lock:
|
344
|
+
distribution = {}
|
345
|
+
for edge in self._edges.values():
|
346
|
+
weight_str = str(edge.weight)
|
347
|
+
distribution[weight_str] = distribution.get(weight_str, 0) + 1
|
348
|
+
return distribution
|
349
|
+
|
350
|
+
def get_heavy_edges(self, threshold: float) -> Iterator[WeightedEdge]:
|
351
|
+
"""Get all edges with weight above threshold."""
|
352
|
+
with self._lock:
|
353
|
+
for edge in self._edges.values():
|
354
|
+
if edge.weight > threshold:
|
355
|
+
yield edge
|
356
|
+
|
357
|
+
def get_light_edges(self, threshold: float) -> Iterator[WeightedEdge]:
|
358
|
+
"""Get all edges with weight below threshold."""
|
359
|
+
with self._lock:
|
360
|
+
for edge in self._edges.values():
|
361
|
+
if edge.weight < threshold:
|
362
|
+
yield edge
|
363
|
+
|
364
|
+
def normalize_weights(self, target_min: float = 0.0, target_max: float = 1.0) -> None:
|
365
|
+
"""Normalize all edge weights to target range."""
|
366
|
+
if not self._edges:
|
367
|
+
return
|
368
|
+
|
369
|
+
with self._lock:
|
370
|
+
# Find current min and max weights
|
371
|
+
current_min = min(edge.weight for edge in self._edges.values())
|
372
|
+
current_max = max(edge.weight for edge in self._edges.values())
|
373
|
+
|
374
|
+
if current_min == current_max:
|
375
|
+
# All weights are the same, set to target_min
|
376
|
+
for edge in self._edges.values():
|
377
|
+
edge.weight = target_min
|
378
|
+
self._adjacency[edge.source][edge.target] = target_min
|
379
|
+
self._reverse_adjacency[edge.target][edge.source] = target_min
|
380
|
+
else:
|
381
|
+
# Normalize weights
|
382
|
+
for edge in self._edges.values():
|
383
|
+
normalized_weight = target_min + (edge.weight - current_min) * (target_max - target_min) / (current_max - current_min)
|
384
|
+
edge.weight = self._normalize_weight(normalized_weight)
|
385
|
+
self._adjacency[edge.source][edge.target] = edge.weight
|
386
|
+
self._reverse_adjacency[edge.target][edge.source] = edge.weight
|
387
|
+
|
388
|
+
# Update statistics
|
389
|
+
self._min_weight = target_min
|
390
|
+
self._max_weight = target_max
|
391
|
+
|
392
|
+
def get_stats(self) -> Dict[str, Any]:
|
393
|
+
"""Get performance statistics."""
|
394
|
+
with self._lock:
|
395
|
+
return {
|
396
|
+
'edge_count': self._edge_count,
|
397
|
+
'node_count': self.get_node_count(),
|
398
|
+
'total_edges_added': self._total_edges_added,
|
399
|
+
'total_edges_removed': self._total_edges_removed,
|
400
|
+
'total_weight_updates': self._total_weight_updates,
|
401
|
+
'min_weight': self._min_weight if self._min_weight != float('inf') else 0.0,
|
402
|
+
'max_weight': self._max_weight,
|
403
|
+
'average_weight': self.get_average_weight(),
|
404
|
+
'total_weight': self.get_total_weight(),
|
405
|
+
'directed': self.directed,
|
406
|
+
'default_weight': self.default_weight,
|
407
|
+
'weight_precision': self.weight_precision,
|
408
|
+
'strategy': 'WEIGHTED_GRAPH',
|
409
|
+
'backend': 'Weighted graph with numerical edge weights',
|
410
|
+
'traits': [trait.name for trait in EdgeTrait if self.has_trait(trait)]
|
411
|
+
}
|